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

เมธอด (Methods)

เมธอดมีลักษณะคล้ายกับฟังก์ชัน: เราประกาศเมธอดด้วยคีย์เวิร์ด fn และตามด้วยชื่อ สามารถมีพารามิเตอร์และค่าส่งกลับได้ รวมถึงมีโค้ดบางส่วนที่จะถูกรันเมื่อเมธอดนั้นถูกเรียกใช้งานจากที่อื่น แต่สิ่งที่ต่างจากฟังก์ชันคือ เมธอดจะถูกกำหนดขึ้นภายใต้บริบทของโครงสร้างข้อมูล (struct) (หรือ enum หรือ trait object ซึ่งเราจะกล่าวถึงในบทที่ 6 และบทที่ 18 ตามลำดับ) และพารามิเตอร์แรกของเมธอดจะเป็น self เสมอ ซึ่งเป็นตัวแทนของอินสแตนซ์ของ struct ที่เมธอดนั้นกำลังถูกเรียกใช้งาน

ไวยากรณ์เมธอด

เรามาลองเปลี่ยนฟังก์ชัน area ที่รับอินสแตนซ์ของ Rectangle เป็นพารามิเตอร์ ให้กลายเป็นเมธอด area ที่ถูกกำหนดไว้บน struct Rectangle แทน ดังแสดงในรายการโค้ดที่ 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()
    );
}

ในการกำหนดฟังก์ชันภายใต้บริบทของ Rectangle เราจะเริ่มด้วยการสร้างบล็อก impl (implementation) สำหรับ Rectangle โดยทุกอย่างภายในบล็อก impl นี้จะเชื่อมโยงกับชนิดข้อมูล Rectangle จากนั้นเราจะย้ายฟังก์ชัน area เข้าไปไว้ในวงเล็บปีกกาของ impl และเปลี่ยนพารามิเตอร์แรก (ซึ่งในกรณีนี้คือพารามิเตอร์เดียว) ให้เป็น self ในส่วนหัวประกาศ (signature) และทุกจุดภายในตัวฟังก์ชัน (body) ในฟังก์ชัน main ที่เราเคยเรียกใช้ฟังก์ชัน area และส่ง rect1 เป็นอาร์กิวเมนต์ เราสามารถเปลี่ยนมาใช้ ไวยากรณ์เมธอด (method syntax) เพื่อเรียกเมธอด area บนอินสแตนซ์ของ Rectangle แทนได้ ไวยากรณ์เมธอดจะอยู่ต่อท้ายอินสแตนซ์ โดยเราจะใส่จุดตามด้วยชื่อเมธอด วงเล็บ และอาร์กิวเมนต์ต่าง ๆ (ถ้ามี)

ในส่วนหัวของเมธอด area เราใช้ &self แทน rectangle: &Rectangle ซึ่งอันที่จริงแล้ว &self เป็นรูปย่อของ self: &Self โดยภายในบล็อก impl ชนิดข้อมูล Self จะเป็นนามแฝง (alias) แทนชนิดข้อมูลที่บล็อก impl นั้นเขียนขึ้นเพื่อมัน เมธอดจะต้องมีพารามิเตอร์แรกชื่อ self ที่มีชนิดข้อมูลเป็น Self ดังนั้น Rust จึงอนุญาตให้คุณย่อเหลือเพียงแค่ชื่อ self ในตำแหน่งพารามิเตอร์แรกได้ ทั้งนี้ สังเกตว่าเรายังคงต้องใช้เครื่องหมาย & ไว้หน้าคำย่อ self เพื่อระบุว่าเมธอดนี้ทำการขอยืม (borrow) อินสแตนซ์ของ Self เช่นเดียวกับที่เราทำใน rectangle: &Rectangle เมธอดสามารถรับสิทธิ์ความเป็นเจ้าของ (ownership) ของ self หรือทำการขอยืม self แบบอ่านอย่างเดียว (immutably) ดังที่เราทำในตัวอย่างนี้ หรือทำการขอยืม self แบบแก้ไขได้ (mutably) ก็ได้เช่นเดียวกับพารามิเตอร์อื่น ๆ

เราเลือกใช้ &self ในที่นี้ด้วยเหตุผลเดียวกับที่เราใช้ &Rectangle ในแบบฟังก์ชัน นั่นคือเราไม่ต้องการครอบครองสิทธิ์ความเป็นเจ้าของ (ownership) และเราเพียงต้องการอ่านข้อมูลใน struct เท่านั้น ไม่ต้องการเขียนหรือแก้ไขข้อมูล หากเราต้องการแก้ไขอินสแตนซ์ที่เราเรียกใช้งานเมธอดในฐานะส่วนหนึ่งของการทำงานของเมธอด เราจะใช้ &mut self เป็นพารามิเตอร์แรก ทั้งนี้ เมธอดที่รับสิทธิ์ความเป็นเจ้าของอินสแตนซ์ด้วยการใช้ self เพียงอย่างเดียวเป็นพารามิเตอร์แรกนั้นเกิดขึ้นได้ค่อนข้างยาก โดยทั่วไปเทคนิคนี้จะใช้เมื่อเมธอดนั้นแปลง self ไปเป็นอย่างอื่น และคุณต้องการป้องกันไม่ให้ผู้เรียกใช้กลับมาใช้งานอินสแตนซ์เดิมหลังจากการแปลงดังกล่าว

เหตุผลหลักในการเลือกใช้เมธอดแทนฟังก์ชัน นอกเหนือจากการช่วยอำนวยความสะดวกในเรื่องไวยากรณ์เมธอดและช่วยให้ไม่ต้องเขียนระบุชนิดข้อมูลของ self ซ้ำในทุก ๆ เมธอดแล้ว ยังเป็นเรื่องของการจัดระเบียบโค้ดอีกด้วย เพราะช่วยให้เราสามารถรวมทุกสิ่งที่เราทำได้กับอินสแตนซ์ของชนิดข้อมูลหนึ่ง ๆ ไว้ในบล็อก impl เดียว แทนที่จะต้องให้ผู้ที่จะนำโค้ดของเราไปใช้ในอนาคตต้องคอยค้นหาความสามารถต่าง ๆ ของ Rectangle จากที่ต่าง ๆ ในไลบรารีที่เราจัดเตรียมไว้

นอกจากนี้ เรายังสามารถเลือกตั้งชื่อเมธอดให้เหมือนกับชื่อฟิลด์ใดฟิลด์หนึ่งของ 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 คืนค่าเป็น true หากค่าในฟิลด์ width ของอินสแตนซ์มีค่ามากกว่า 0 และคืนค่าเป็น false หากค่านั้นเป็น 0 ซึ่งเราสามารถใช้ฟิลด์ภายในเมธอดที่มีชื่อเดียวกันเพื่อวัตถุประสงค์ใด ๆ ก็ได้ ในฟังก์ชัน main เมื่อเราเรียกใช้ rect1.width และตามด้วยวงเล็บ Rust จะรู้ว่าเราหมายถึงเมธอด width แต่ถ้าเราไม่ใส่วงเล็บ Rust จะรู้ว่าเราหมายถึงฟิลด์ width แทน

บ่อยครั้ง (แต่ไม่เสมอไป) ที่การตั้งชื่อเมธอดให้เหมือนกับชื่อฟิลด์มีจุดประสงค์เพื่อให้มันคืนค่าในฟิลด์นั้น ๆ ออกมาเท่านั้นโดยไม่ต้องทำอย่างอื่น เมธอดในลักษณะนี้จะเรียกว่า เก็ตเตอร์ (getters) ซึ่งภาษา Rust ไม่ได้สร้างเก็ตเตอร์ให้อัตโนมัติสำหรับฟิลด์ของ struct เหมือนอย่างที่บางภาษาทำ เก็ตเตอร์มีประโยชน์เนื่องจากคุณสามารถกำหนดให้ฟิลด์นั้นเป็นแบบส่วนตัว (private) แต่กำหนดให้เมธอดเป็นแบบสาธารณะ (public) ได้ ซึ่งจะช่วยให้ผู้ใช้สามารถอ่านข้อมูลในฟิลด์นั้นได้อย่างเดียว (read-only) ผ่านทาง API สาธารณะของชนิดข้อมูลดังกล่าว เราจะอภิปรายเพิ่มเติมเกี่ยวกับการกำหนดสิทธิ์การเข้าถึงแบบ public และ private รวมถึงการกำหนดให้ฟิลด์หรือเมธอดเป็นแบบสาธารณะหรือส่วนตัวในบทที่ 7

ตัวดำเนินการ -> อยู่ที่ไหน?

ในภาษา C และ C++ จะใช้ตัวดำเนินการ 2 แบบที่แตกต่างกันในการเรียกใช้เมธอด ได้แก่ คุณจะใช้ . หากเรียกใช้เมธอดบนตัวอ็อบเจกต์โดยตรง และใช้ -> หากคุณเรียกใช้เมธอดบนตัวชี้ (pointer) ไปยังอ็อบเจกต์และจำเป็นต้องทำการลดระดับตัวชี้ (dereference) ก่อน พูดอีกนัยหนึ่งคือ ถ้า object เป็นตัวชี้ การเรียก object->something() จะเทียบเท่ากับ (*object).something()

ภาษา Rust ไม่มีตัวดำเนินการที่เทียบเท่ากับ -> แต่ Rust จะมีฟีเจอร์ที่เรียกว่า การอ้างอิงและลดระดับตัวชี้อัตโนมัติ (automatic referencing and dereferencing) ซึ่งการเรียกใช้งานเมธอดก็เป็นหนึ่งในไม่กี่แห่งใน Rust ที่มีพฤติกรรมในลักษณะนี้

วิธีการทำงานคือ เมื่อคุณเรียกเมธอดด้วย object.something() ทาง Rust จะใส่เครื่องหมาย &, &mut หรือ * ให้โดยอัตโนมัติ เพื่อให้ object มีรูปแบบข้อมูลตรงตามหัวประกาศ (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);
}

รูปแบบแรกนั้นดูสะอาดตาและอ่านง่ายกว่ามาก พฤติกรรมการสร้างการอ้างอิงอัตโนมัตินี้ทำงานได้เนื่องจากเมธอดมีผู้รับ (receiver) ที่ชัดเจน ซึ่งก็คือชนิดข้อมูลของ self เมื่อทราบข้อมูลผู้รับและชื่อของเมธอดแล้ว Rust จะสามารถระบุได้อย่างแน่ชัดว่าเมธอดดังกล่าวเป็นแบบอ่านอย่างเดียว (&self) แบบแก้ไขข้อมูล (&mut self) หรือแบบทำลายค่า (self) การที่ Rust ทำให้การขอยืม (borrowing) เป็นแบบโดยนัย (implicit) สำหรับตัวรับเมธอดนั้น ถือเป็นส่วนสำคัญที่ช่วยให้ระบบความเป็นเจ้าของ (ownership) สามารถใช้งานได้สะดวกสบายยิ่งขึ้นในทางปฏิบัติ

เมธอดที่มีพารามิเตอร์เพิ่มขึ้น

เรามาฝึกฝนการใช้เมธอดกันต่อโดยการเพิ่มเมธอดที่สองบน struct Rectangle โดยในครั้งนี้เราต้องการให้อินสแตนซ์หนึ่งของ Rectangle รับอินสแตนซ์อีกตัวของ Rectangle เข้ามา แล้วส่งกลับค่าเป็น true หากอินสแตนซ์ของ Rectangle ตัวที่สองสามารถบรรจุอยู่ภายใน self (อินสแตนซ์แรกของ Rectangle) ได้ทั้งหมดพอดี แต่ถ้าไม่สามารถบรรจุได้ ก็จะส่งกลับเป็น false ซึ่งหลังจากที่เราเขียนเมธอด can_hold นี้แล้ว เราต้องการให้สามารถเขียนโปรแกรมได้ดังที่แสดงในรายการโค้ดที่ 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));
}

ผลลัพธ์ที่คาดหวังจะแสดงออกมาในลักษณะต่อไปนี้ เนื่องจากขนาดทั้งสองมิติของ rect2 มีขนาดเล็กกว่าขนาดของ rect1 แต่ rect3 มีความกว้างมากกว่า rect1:

Can rect1 hold rect2? true
Can rect1 hold rect3? false

เรารู้ว่าต้องการสร้างเมธอด ดังนั้นจึงต้องเขียนไว้ภายในบล็อก impl Rectangle โดยชื่อเมธอดจะเป็น can_hold และจะรับการอ้างอิงขอยืมแบบแก้ไขไม่ได้ (immutable borrow) ของ Rectangle อีกตัวเข้ามาเป็นพารามิเตอร์ ซึ่งเราสามารถเดาชนิดข้อมูลของพารามิเตอร์นี้ได้จากการดูโค้ดตรงจุดที่เรียกใช้เมธอด ได้แก่ rect1.can_hold(&rect2) ที่ส่งค่า &rect2 เข้ามา ซึ่งหมายถึงการขอยืมแบบอ่านอย่างเดียวไปยัง rect2 ที่เป็นอินสแตนซ์ของ Rectangle เรื่องนี้สมเหตุสมผลเนื่องจากเราต้องการเพียงแค่อ่านข้อมูลจาก rect2 (ไม่ใช่ต้องการเขียนทับ ซึ่งถ้าเป็นแบบนั้นเราจะต้องใช้อ้างอิงแบบแก้ไขได้) และเราต้องการให้ main ยังคงมีความเป็นเจ้าของ rect2 อยู่ เพื่อที่เราจะนำมันไปใช้งานต่อได้หลังจากเรียกเมธอด can_hold เสร็จสิ้นแล้ว ส่วนค่าส่งกลับของ can_hold จะเป็นชนิดข้อมูลแบบบูลีน (Boolean) และส่วนการทำงานภายในจะตรวจสอบว่าทั้งความกว้างและความสูงของ self มีค่ามากกว่าความกว้างและความสูงของ Rectangle อีกตัวหนึ่งตามลำดับหรือไม่ เรามาเพิ่มเมธอดใหม่ can_hold นี้ลงในบล็อก impl จากรายการโค้ดที่ 5-13 ดังที่แสดงในรายการโค้ดที่ 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));
}

เมื่อเรารันโค้ดชุดนี้พร้อมกับฟังก์ชัน main ในรายการโค้ดที่ 5-14 เราจะได้ผลลัพธ์ตามที่ต้องการ เมธอดต่าง ๆ สามารถรับพารามิเตอร์ได้หลายตัวโดยเพิ่มลงในหัวประกาศต่อจากพารามิเตอร์ self และพารามิเตอร์เหล่านั้นก็จะทำงานเหมือนพารามิเตอร์ในฟังก์ชันปกติทุกประการ

ฟังก์ชันที่เกี่ยวเนื่อง (Associated Functions)

ฟังก์ชันทั้งหมดที่กำหนดขึ้นภายในบล็อก impl จะเรียกว่า ฟังก์ชันที่เกี่ยวเนื่อง (associated functions) เนื่องจากฟังก์ชันเหล่านั้นมีความเชื่อมโยงกับชนิดข้อมูลที่ระบุไว้หลังคำว่า impl เราสามารถสร้างฟังก์ชันที่เกี่ยวเนื่องที่ไม่มีพารามิเตอร์แรกเป็น self ได้ (ซึ่งนั่นทำให้พวกมันไม่ใช่เมธอด) เนื่องจากพวกมันไม่ต้องการอินสแตนซ์ของชนิดข้อมูลนั้น ๆ ในการทำงาน ตัวอย่างเช่นเราเคยเรียกใช้ฟังก์ชันลักษณะนี้มาแล้ว คือฟังก์ชัน String::from ที่ถูกประกาศอยู่บนชนิดข้อมูล String

ฟังก์ชันที่เกี่ยวเนื่องที่ไม่มีสถานะเป็นเมธอด มักถูกนำมาใช้เป็นตัวสร้างอินสแตนซ์ (constructors) เพื่อคืนค่าเป็นอินสแตนซ์ใหม่ของ struct ซึ่งฟังก์ชันเหล่านี้มักตั้งชื่อว่า new แต่อย่างไรก็ตาม ชื่อ new ไม่ได้เป็นชื่อพิเศษหรือฟีเจอร์สำเร็จรูปของภาษาแต่อย่างใด ตัวอย่างเช่น เราสามารถเลือกให้บริการฟังก์ชันที่เกี่ยวเนื่องที่ชื่อว่า square ซึ่งรับพารามิเตอร์ความยาวด้านเพียงหนึ่งตัว และใช้ค่านั้นเป็นทั้งความกว้างและความสูง ซึ่งจะช่วยให้สร้างอินสแตนซ์ Rectangle ที่เป็นรูปสี่เหลี่ยมจัตุรัสได้ง่ายขึ้น โดยไม่ต้องระบุค่าเดิมซ้ำสองครั้ง:

ชื่อไฟล์: 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);
}

คีย์เวิร์ด Self ทั้งในชนิดข้อมูลที่ส่งกลับและภายในตัวฟังก์ชัน คือนามแฝงของชนิดข้อมูลที่ปรากฏหลังคีย์เวิร์ด impl ซึ่งในกรณีนี้ก็คือ Rectangle

ในการเรียกใช้งานฟังก์ชันที่เกี่ยวเนื่องนี้ เราจะใช้ไวยากรณ์ :: ร่วมกับชื่อ struct เช่น let sq = Rectangle::square(3); ฟังก์ชันนี้ถูกจัดให้อยู่ในขอบเขตชื่อ (namespace) ของ struct โดยไวยากรณ์ :: นั้นถูกใช้ทั้งในการเรียกใช้ฟังก์ชันที่เกี่ยวเนื่อง และสำหรับการเข้าถึงขอบเขตชื่อที่ถูกสร้างขึ้นโดยโมดูล (module) เราจะคุยเรื่องโมดูลอย่างละเอียดในบทที่ 7

บล็อก impl หลายบล็อก

โครงสร้างข้อมูลแต่ละตัวสามารถมีบล็อก impl ได้หลายบล็อก ตัวอย่างเช่น รายการโค้ดที่ 5-15 จะทำงานเทียบเท่ากับโค้ดที่แสดงในรายการโค้ดที่ 5-16 ซึ่งกำหนดให้แต่ละเมธอดอยู่แยกจากกันในบล็อก 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 หลายบล็อกในสถานการณ์ปัจจุบัน แต่นี่คือไวยากรณ์ที่ใช้การได้จริงและถูกต้อง เราจะได้เรียนรู้ถึงกรณีที่บล็อก impl หลายบล็อกจะมีประโยชน์จริง ๆ ในบทที่ 10 ซึ่งเราจะศึกษาเกี่ยวกับชนิดข้อมูลแบบทั่วไป (generic types) และเทรต (traits)

สรุป

โครงสร้างข้อมูล (structs) ช่วยให้คุณสร้างชนิดข้อมูลขึ้นมาใช้เองตามความเหมาะสมของงานที่คุณกำลังทำ การเลือกใช้งาน struct จะช่วยเก็บรวบรวมชิ้นส่วนข้อมูลที่มีความเชื่อมโยงกันเอาไว้ด้วยกัน และสามารถตั้งชื่อในแต่ละส่วนเพื่อทำความเข้าใจโค้ดได้ง่ายขึ้น ภายในบล็อก impl คุณจะสามารถกำหนดฟังก์ชันต่าง ๆ ที่เกี่ยวเนื่องกับชนิดข้อมูลของคุณได้ และเมธอดก็คือฟังก์ชันที่เกี่ยวเนื่องประเภทหนึ่งที่ช่วยให้คุณระบุพฤติกรรมที่จะทำได้ของอินสแตนซ์ของ struct เหล่านั้น

อย่างไรก็ตาม struct ไม่ใช่วิธีเดียวที่คุณจะสร้างชนิดข้อมูลของตัวเองขึ้นมาได้ เรามาทำความรู้จักกับฟีเจอร์ enum ของภาษา Rust เพื่อเพิ่มเครื่องมืออีกชิ้นลงในกล่องเครื่องมือของคุณกันเถอะ