เมธอด
เมธอดคล้ายกับฟังก์ชัน — เราประกาศพวกมันด้วย keyword fn และชื่อ พวกมัน
มี parameter และ return value ได้ และมีโค้ดที่รันเมื่อเมธอดถูกเรียกจาก
ที่อื่น ต่างจากฟังก์ชัน เมธอดถูกประกาศภายในบริบทของ struct (หรือ enum
หรือ trait object ซึ่งเราครอบคลุมใน บทที่ 6 และ
บทที่ 18 ตามลำดับ) และ parameter แรกของ
พวกมันคือ self เสมอ ซึ่งแทน instance ของ struct ที่เมธอดถูกเรียกใน
Method Syntax
มาเปลี่ยนฟังก์ชัน area ที่มี instance Rectangle เป็น parameter และ
แทนนั้นทำเป็นเมธอด area ที่ประกาศบน struct Rectangle ดังที่แสดงใน
Listing 5-13
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}
area บน struct Rectangleในการประกาศฟังก์ชันภายในบริบทของ Rectangle เราเริ่ม block impl
(implementation) สำหรับ Rectangle ทุกอย่างภายใน block impl นี้จะ
ผูกกับ type Rectangle จากนั้นเราย้ายฟังก์ชัน area เข้า curly bracket
ของ impl และเปลี่ยน parameter แรก (และในกรณีนี้คือเดียว) ให้เป็น
self ใน signature และทุกที่ภายใน body ใน main ที่เราเรียกฟังก์ชัน
area และส่ง rect1 เป็น argument เราใช้ method syntax เรียกเมธอด
area บน instance Rectangle ของเราได้แทน method syntax ไปหลัง instance
— เราเพิ่ม dot ตามด้วยชื่อเมธอด วงเล็บ และ argument ใด ๆ
ใน signature ของ area เราใช้ &self แทน rectangle: &Rectangle
&self จริง ๆ เป็น shorthand ของ self: &Self ภายใน block impl type
Self เป็น alias ของ type ที่ block impl เป็น เมธอดต้องมี parameter
ชื่อ self ของ type Self เป็น parameter แรก Rust จึงให้คุณย่อด้วยแค่
ชื่อ self ในตำแหน่ง parameter แรก หมายเหตุว่าเรายังต้องใช้ & หน้า
shorthand self เพื่อบ่งบอกว่าเมธอดนี้ borrow instance Self เหมือนที่
เราทำใน rectangle: &Rectangle เมธอดรับ ownership ของ self, borrow
self แบบ immutable อย่างที่เราทำที่นี่, หรือ borrow self แบบ mutable
ได้ เหมือนที่ทำกับ parameter อื่นใด
เราเลือก &self ที่นี่ด้วยเหตุผลเดียวกับที่เราใช้ &Rectangle ใน version
ฟังก์ชัน — เราไม่อยากรับ ownership และเราแค่อยากอ่านข้อมูลใน struct ไม่
ใช่เขียน ถ้าเราอยากเปลี่ยน instance ที่เราเรียกเมธอดด้วยเป็นส่วนหนึ่งของ
สิ่งที่เมธอดทำ เราจะใช้ &mut self เป็น parameter แรก การมีเมธอดที่รับ
ownership ของ instance โดยใช้แค่ self เป็น parameter แรกเป็นเรื่องหา
ยาก เทคนิคนี้มักใช้เมื่อเมธอด transform self เป็นอย่างอื่น และคุณอยาก
ป้องกัน caller ใช้ instance เดิมหลัง transformation
เหตุผลหลักของการใช้เมธอดแทนฟังก์ชัน นอกจากให้ method syntax และไม่ต้อง
เขียน type ของ self ซ้ำใน signature ของทุกเมธอด คือเพื่อการจัดระเบียบ
เราใส่ทุกสิ่งที่เราทำได้กับ instance ของ type ใน block impl เดียว แทน
การให้ user ในอนาคตของโค้ดเราหา capability ของ Rectangle ในที่ต่าง ๆ
ใน library ที่เราให้
หมายเหตุว่าเราเลือกให้เมธอดมีชื่อเหมือนหนึ่งใน field ของ struct ได้ เช่น
เราประกาศเมธอดบน Rectangle ที่ชื่อ width ด้วยได้:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn width(&self) -> bool {
self.width > 0
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
if rect1.width() {
println!("The rectangle has a nonzero width; it is {}", rect1.width);
}
}
ที่นี่ เราเลือกให้เมธอด width return true ถ้าค่าใน field width ของ
instance มากกว่า 0 และ false ถ้าค่าเป็น 0 — เราใช้ field ภายในเมธอด
ที่ชื่อเดียวกันได้สำหรับจุดประสงค์ใด ๆ ใน main เมื่อเราตาม rect1.width
ด้วยวงเล็บ Rust รู้ว่าเราหมายถึงเมธอด width เมื่อเราไม่ใช้วงเล็บ Rust
รู้ว่าเราหมายถึง field width
บ่อยครั้ง แต่ไม่เสมอ เมื่อเราให้เมธอดมีชื่อเหมือน field เราอยากให้มัน return แค่ค่าใน field และไม่ทำอะไรอื่น เมธอดแบบนี้เรียกว่า getter และ Rust ไม่ implement พวกมันอัตโนมัติสำหรับ field ของ struct เหมือนภาษาอื่น บางตัวทำ Getter มีประโยชน์เพราะคุณทำให้ field เป็น private ได้ แต่เมธอด เป็น public และจึงเปิดทางให้เข้าถึงแบบ read-only ของ field นั้นเป็นส่วน หนึ่งของ public API ของ type เราจะพูดถึงว่า public และ private คืออะไร และวิธีกำหนด field หรือเมธอดเป็น public หรือ private ใน บทที่ 7
Operator -> อยู่ไหน?
ใน C และ C++ มี operator ต่างกันสองตัวที่ใช้เรียกเมธอด — คุณใช้ . ถ้า
กำลังเรียกเมธอดบน object ตรง ๆ และ -> ถ้ากำลังเรียกเมธอดบน pointer
ของ object และต้อง dereference pointer ก่อน พูดอีกอย่าง ถ้า object
เป็น pointer object->something() คล้ายกับ (*object).something()
Rust ไม่มีสิ่งเทียบเท่า operator -> แทนนั้น Rust มีฟีเจอร์ชื่อ
automatic referencing และ dereferencing การเรียกเมธอดเป็นหนึ่งใน
ไม่กี่ที่ใน Rust ที่มีพฤติกรรมนี้
นี่คือวิธีที่มันทำงาน — เมื่อคุณเรียกเมธอดด้วย object.something()
Rust เพิ่ม &, &mut หรือ * อัตโนมัติ เพื่อให้ object match
signature ของเมธอด พูดอีกอย่าง สิ่งต่อไปนี้เหมือนกัน:
#![allow(unused)]
fn main() {
#[derive(Debug,Copy,Clone)]
struct Point {
x: f64,
y: f64,
}
impl Point {
fn distance(&self, other: &Point) -> f64 {
let x_squared = f64::powi(other.x - self.x, 2);
let y_squared = f64::powi(other.y - self.y, 2);
f64::sqrt(x_squared + y_squared)
}
}
let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 5.0, y: 6.5 };
p1.distance(&p2);
(&p1).distance(&p2);
}
ตัวแรกดูสะอาดกว่ามาก พฤติกรรมการ reference อัตโนมัตินี้ทำงานได้ เพราะ
เมธอดมี receiver ที่ชัดเจน — type ของ self เมื่อมี receiver และชื่อ
ของเมธอด Rust รู้แน่นอนได้ว่าเมธอดกำลังอ่าน (&self), mutate
(&mut self) หรือ consume (self) ข้อเท็จจริงที่ว่า Rust ทำให้
borrowing เป็น implicit สำหรับ method receiver เป็นส่วนใหญ่ที่ทำให้
ownership ใช้สะดวกในทางปฏิบัติ
เมธอดที่มี Parameter เพิ่มเติม
มาฝึกใช้เมธอดโดย implement เมธอดที่สองบน struct Rectangle ครั้งนี้เรา
อยากให้ instance ของ Rectangle รับ instance อื่นของ Rectangle แล้ว
return true ถ้า Rectangle ที่สองพอดีอย่างสมบูรณ์ภายใน self
(Rectangle แรก) ไม่อย่างนั้นควร return false นั่นคือ เมื่อเราประกาศ
เมธอด can_hold แล้ว เราอยากเขียนโปรแกรมที่แสดงใน Listing 5-14 ได้
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
can_hold ที่ยังไม่เขียนOutput ที่คาดจะหน้าตาเป็นต่อไปนี้ เพราะมิติของ rect2 ทั้งคู่เล็กกว่า
มิติของ rect1 แต่ rect3 กว้างกว่า rect1:
Can rect1 hold rect2? true
Can rect1 hold rect3? false
เรารู้ว่าอยากประกาศเมธอด มันจึงอยู่ใน block impl Rectangle ชื่อเมธอด
จะเป็น can_hold และจะรับ immutable borrow ของ Rectangle อีกตัวเป็น
parameter เราบอกได้ว่า type ของ parameter จะเป็นอะไรโดยดูที่โค้ดที่เรียก
เมธอด — rect1.can_hold(&rect2) ส่ง &rect2 ซึ่งเป็น immutable borrow
ของ rect2, instance ของ Rectangle นี่สมเหตุสมผล เพราะเราต้องการแค่
อ่าน rect2 (แทนการเขียน ซึ่งจะหมายความว่าเราต้องการ mutable borrow)
และเราอยากให้ main เก็บ ownership ของ rect2 ไว้ เพื่อเราใช้ได้อีกหลัง
เรียกเมธอด can_hold Return value ของ can_hold จะเป็น Boolean และ
implementation จะเช็คว่า width และ height ของ self มากกว่า width และ
height ของ Rectangle อีกตัวตามลำดับ มาเพิ่มเมธอด can_hold ใหม่ใน
block impl จาก Listing 5-13 แสดงใน Listing 5-15
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
can_hold บน Rectangle ที่รับ instance Rectangle อีกตัวเป็น parameterเมื่อเรารันโค้ดนี้ด้วยฟังก์ชัน main ใน Listing 5-14 เราจะได้ output ที่
ต้องการ เมธอดรับ parameter หลายตัวที่เราเพิ่มใน signature หลัง parameter
self ได้ และ parameter เหล่านั้นทำงานเหมือน parameter ในฟังก์ชัน
Associated Function
ฟังก์ชันทั้งหมดที่ประกาศภายใน block impl เรียกว่า associated function
เพราะพวกมันผูกกับ type ที่ตั้งชื่อหลัง impl เราประกาศ associated function
ที่ไม่มี self เป็น parameter แรกได้ (และจึงไม่ใช่เมธอด) เพราะพวกมันไม่
ต้องการ instance ของ type ที่จะทำงานด้วย เราใช้ฟังก์ชันแบบนี้มาแล้ว — ฟังก์ชัน
String::from ที่ประกาศบน type String
Associated function ที่ไม่ใช่เมธอด มักใช้สำหรับ constructor ที่จะ return
instance ใหม่ของ struct มักเรียกว่า new แต่ new ไม่ใช่ชื่อพิเศษและไม่
ได้ built-in ในภาษา เช่น เราเลือกให้ associated function ชื่อ square ที่
มี parameter มิติหนึ่งตัว และใช้นั้นเป็นทั้ง width และ height จึงทำให้ง่าย
ขึ้นที่จะสร้าง Rectangle รูปสี่เหลี่ยมจัตุรัส แทนการต้องระบุค่าเดียวกัน
สองครั้ง:
Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn square(size: u32) -> Self {
Self {
width: size,
height: size,
}
}
}
fn main() {
let sq = Rectangle::square(3);
}
Keyword Self ใน return type และใน body ของฟังก์ชัน เป็น alias ของ type
ที่ปรากฏหลัง keyword impl ซึ่งในกรณีนี้คือ Rectangle
ในการเรียก associated function นี้ เราใช้ syntax :: กับชื่อ struct —
let sq = Rectangle::square(3); เป็นตัวอย่าง ฟังก์ชันนี้อยู่ใน namespace
ของ struct — syntax :: ใช้สำหรับทั้ง associated function และ namespace
ที่สร้างโดย module เราจะพูดถึง module ใน บทที่ 7
Block impl หลายตัว
แต่ละ struct อนุญาตให้มี block impl หลายตัว เช่น Listing 5-15 เทียบเท่า
กับโค้ดที่แสดงใน Listing 5-16 ซึ่งมีแต่ละเมธอดใน block impl ของตัวเอง
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
impl หลายตัวไม่มีเหตุผลที่จะแยกเมธอดเหล่านี้เป็น block impl หลายตัวที่นี่ แต่นี่เป็น
syntax ที่ valid เราจะเห็นกรณีที่ block impl หลายตัวมีประโยชน์ในบทที่
10 ที่เราพูดถึง generic type และ trait
สรุป
Struct ให้คุณสร้าง type custom ที่มีความหมายสำหรับ domain ของคุณ ด้วยการ
ใช้ struct คุณเก็บชิ้นข้อมูลที่ผูกกันให้เชื่อมโยงกัน และตั้งชื่อแต่ละชิ้น
เพื่อทำให้โค้ดของคุณชัดเจน ใน block impl คุณประกาศฟังก์ชันที่ผูกกับ
type ของคุณได้ และเมธอดเป็น associated function ชนิดหนึ่งที่ให้คุณระบุ
พฤติกรรมที่ instance ของ struct ของคุณมี
แต่ struct ไม่ใช่วิธีเดียวที่คุณสร้าง type custom ได้ — มาดูฟีเจอร์ enum ของ Rust เพื่อเพิ่มเครื่องมืออีกตัวให้กล่องเครื่องมือของคุณ