Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

เมธอด

เมธอดคล้ายกับฟังก์ชัน — เราประกาศพวกมันด้วย 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

Filename: src/main.rs
#[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()
    );
}
Listing 5-13: ประกาศเมธอด 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 ด้วยได้:

Filename: src/main.rs
#[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 ได้

Filename: src/main.rs
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));
}
Listing 5-14: ใช้เมธอด 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

Filename: src/main.rs
#[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));
}
Listing 5-15: Implement เมธอด 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));
}
Listing 5-16: เขียน Listing 5-15 ใหม่ด้วย block 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 เพื่อเพิ่มเครื่องมืออีกตัวให้กล่องเครื่องมือของคุณ