Cú pháp Method

Method (phương thức) tương tự như function (hàm): chúng ta khai báo chúng bằng từ khóa fn và một cái tên, chúng có thể có tham số và giá trị trả về, và chúng chứa một số mã lệnh được chạy khi method được gọi từ một nơi khác. Không giống như function, method được định nghĩa trong ngữ cảnh của một struct (hoặc một enum hay một trait object, chúng ta sẽ đề cập trong Chương 6Chương 18), và tham số đầu tiên của chúng luôn là self, đại diện cho thực thể của struct mà method đang được gọi trên đó.

Định nghĩa Method

Hãy thay đổi hàm area có một thực thể Rectangle làm tham số và thay vào đó tạo một method area được định nghĩa trên struct Rectangle, như trong 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: Định nghĩa một method area trên struct Rectangle

Để định nghĩa hàm trong ngữ cảnh của Rectangle, chúng ta bắt đầu một khối impl (viết tắt của implementation - triển khai) cho Rectangle. Mọi thứ bên trong khối impl này sẽ được liên kết với kiểu Rectangle. Sau đó, chúng ta di chuyển hàm area vào bên trong cặp ngoặc nhọn của impl và thay đổi tham số đầu tiên (và trong trường hợp này là duy nhất) thành self trong chữ ký và ở mọi nơi trong thân hàm. Trong main, nơi chúng ta đã gọi hàm area và truyền rect1 làm đối số, thay vào đó chúng ta có thể sử dụng cú pháp method để gọi method area trên thực thể Rectangle của mình. Cú pháp method đi sau một thực thể: chúng ta thêm một dấu chấm theo sau là tên method, cặp ngoặc đơn và bất kỳ đối số nào.

Trong chữ ký của area, chúng ta sử dụng &self thay vì rectangle: &Rectangle. &self thực ra là viết tắt của self: &Self. Bên trong một khối impl, kiểu Self là một bí danh cho kiểu mà khối impl đó dành cho. Các method phải có một tham số tên là self của kiểu Self làm tham số đầu tiên, vì vậy Rust cho phép bạn viết tắt điều này chỉ bằng tên self ở vị trí tham số đầu tiên. Lưu ý rằng chúng ta vẫn cần sử dụng & trước self viết tắt để chỉ ra rằng method này mượn thực thể Self, giống như chúng ta đã làm trong rectangle: &Rectangle. Các method có thể lấy quyền sở hữu của self, mượn self một cách bất biến (immutable) như chúng ta đã làm ở đây, hoặc mượn self một cách khả biến (mutable), giống như bất kỳ tham số nào khác.

Chúng ta đã chọn &self ở đây vì cùng lý do chúng ta đã sử dụng &Rectangle trong phiên bản hàm: chúng ta không muốn lấy quyền sở hữu, và chúng ta chỉ muốn đọc dữ liệu trong struct, không phải ghi vào nó. Nếu chúng ta muốn thay đổi thực thể mà chúng ta đã gọi method trên đó như một phần của những gì method làm, chúng ta sẽ sử dụng &mut self làm tham số đầu tiên. Việc có một method lấy quyền sở hữu của thực thể bằng cách chỉ sử dụng self làm tham số đầu tiên là rất hiếm; kỹ thuật này thường được sử dụng khi method biến đổi self thành một thứ khác và bạn muốn ngăn người gọi sử dụng thực thể ban đầu sau khi biến đổi.

Lý do chính để sử dụng method thay vì function, ngoài việc cung cấp cú pháp method và không phải lặp lại kiểu của self trong chữ ký của mỗi method, là để tổ chức. Chúng ta đã đặt tất cả những thứ chúng ta có thể làm với một thực thể của một kiểu vào một khối impl thay vì khiến những người dùng tương lai của mã nguồn của chúng ta phải tìm kiếm các khả năng của Rectangle ở nhiều nơi khác nhau trong thư viện mà chúng ta cung cấp.

Lưu ý rằng chúng ta có thể chọn đặt tên cho một method trùng với tên một trong các trường của struct. Ví dụ, chúng ta có thể định nghĩa một method trên Rectangle cũng có tên là 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);
    }
}

Ở đây, chúng ta chọn làm cho method width trả về true nếu giá trị trong trường width của thực thể lớn hơn 0false nếu giá trị là 0: chúng ta có thể sử dụng một trường bên trong một method cùng tên cho bất kỳ mục đích nào. Trong main, khi chúng ta theo sau rect1.width bằng cặp ngoặc đơn, Rust biết chúng ta muốn nói đến method width. Khi chúng ta không sử dụng cặp ngoặc đơn, Rust biết chúng ta muốn nói đến trường width.

Thường thì, nhưng không phải lúc nào cũng vậy, khi chúng ta đặt tên cho một method trùng với tên một trường, chúng ta muốn nó chỉ trả về giá trị trong trường đó và không làm gì khác. Các method như thế này được gọi là getter, và Rust không tự động triển khai chúng cho các trường của struct như một số ngôn ngữ khác. Getter hữu ích vì bạn có thể đặt trường ở chế độ riêng tư (private) nhưng method ở chế độ công khai (public), và do đó cho phép truy cập chỉ đọc vào trường đó như một phần của API công khai của kiểu. Chúng ta sẽ thảo luận về công khai và riêng tư là gì và cách chỉ định một trường hoặc method là công khai hay riêng tư trong Chương 7.

Toán tử -> ở đâu?

Trong C và C++, hai toán tử khác nhau được sử dụng để gọi method: bạn sử dụng . nếu bạn đang gọi một method trên đối tượng trực tiếp và -> nếu bạn đang gọi method trên một con trỏ tới đối tượng và cần giải tham chiếu con trỏ trước. Nói cách khác, nếu object là một con trỏ, object->something() tương tự như (*object).something().

Rust không có toán tử tương đương với ->; thay vào đó, Rust có một tính năng gọi là tham chiếu và giải tham chiếu tự động (automatic referencing and dereferencing). Gọi method là một trong số ít nơi trong Rust có hành vi này.

Đây là cách nó hoạt động: khi bạn gọi một method với object.something(), Rust tự động thêm vào &, &mut, hoặc * để object khớp với chữ ký của method. Nói cách khác, những điều sau đây là như nhau:

#![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);
}

Cách đầu tiên trông gọn gàng hơn nhiều. Hành vi tham chiếu tự động này hoạt động vì các method có một “người nhận” (receiver) rõ ràng—kiểu của self. Dựa vào người nhận và tên của một method, Rust có thể xác định chắc chắn liệu method đó đang đọc (&self), thay đổi (&mut self), hay tiêu thụ (self). Việc Rust làm cho việc mượn trở nên ngầm định đối với người nhận method là một phần quan trọng giúp cho việc quản lý quyền sở hữu trở nên thuận tiện trong thực tế.

Method với nhiều tham số hơn

Hãy thực hành sử dụng method bằng cách triển khai một method thứ hai trên struct Rectangle. Lần này chúng ta muốn một thực thể của Rectangle nhận một thực thể khác của Rectangle và trả về true nếu Rectangle thứ hai có thể nằm hoàn toàn bên trong self (Rectangle đầu tiên); nếu không, nó sẽ trả về false. Tức là, một khi chúng ta đã định nghĩa method can_hold, chúng ta muốn có thể viết chương trình như trong 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: Sử dụng method can_hold chưa được viết

Kết quả mong đợi sẽ như sau vì cả hai chiều của rect2 đều nhỏ hơn các chiều của rect1, nhưng rect3 lại rộng hơn rect1:

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

Chúng ta biết rằng chúng ta muốn định nghĩa một method, vì vậy nó sẽ nằm trong khối impl Rectangle. Tên method sẽ là can_hold, và nó sẽ nhận một tham chiếu mượn bất biến của một Rectangle khác làm tham số. Chúng ta có thể biết kiểu của tham số bằng cách nhìn vào mã lệnh gọi method: rect1.can_hold(&rect2) truyền vào &rect2, là một tham chiếu mượn bất biến đến rect2, một thực thể của Rectangle. Điều này hợp lý vì chúng ta chỉ cần đọc rect2 (thay vì ghi, điều đó có nghĩa là chúng ta cần một tham chiếu mượn khả biến), và chúng ta muốn main giữ lại quyền sở hữu của rect2 để chúng ta có thể sử dụng lại nó sau khi gọi method can_hold. Giá trị trả về của can_hold sẽ là một Boolean, và phần triển khai sẽ kiểm tra xem chiều rộng và chiều cao của self có lớn hơn chiều rộng và chiều cao của Rectangle kia hay không. Hãy thêm method can_hold mới vào khối impl từ Listing 5-13, được hiển thị trong 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: Triển khai method can_hold trên Rectangle nhận một thực thể Rectangle khác làm tham số

Khi chúng ta chạy mã này với hàm main trong Listing 5-14, chúng ta sẽ nhận được kết quả mong muốn. Các method có thể nhận nhiều tham số mà chúng ta thêm vào chữ ký sau tham số self, và các tham số đó hoạt động giống như các tham số trong function.

Associated Functions (Hàm liên kết)

Tất cả các hàm được định nghĩa trong một khối impl được gọi là associated functions (hàm liên kết) vì chúng được liên kết với kiểu được đặt tên sau impl. Chúng ta có thể định nghĩa các hàm liên kết không có self làm tham số đầu tiên (và do đó không phải là method) vì chúng không cần một thực thể của kiểu để làm việc. Chúng ta đã sử dụng một hàm như thế này: hàm String::from được định nghĩa trên kiểu String.

Các hàm liên kết không phải là method thường được sử dụng cho các hàm khởi tạo (constructor) sẽ trả về một thực thể mới của struct. Chúng thường được gọi là new, nhưng new không phải là một tên đặc biệt và không được tích hợp sẵn trong ngôn ngữ. Ví dụ, chúng ta có thể chọn cung cấp một hàm liên kết tên là square sẽ có một tham số kích thước và sử dụng nó cho cả chiều rộng và chiều cao, do đó giúp việc tạo một Rectangle hình vuông dễ dàng hơn thay vì phải chỉ định cùng một giá trị hai lần:

Tên tệp: 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);
}

Từ khóa Self trong kiểu trả về và trong thân hàm là bí danh cho kiểu xuất hiện sau từ khóa impl, trong trường hợp này là Rectangle.

Để gọi hàm liên kết này, chúng ta sử dụng cú pháp :: với tên struct; let sq = Rectangle::square(3); là một ví dụ. Hàm này được định không gian tên bởi struct: cú pháp :: được sử dụng cho cả các hàm liên kết và các không gian tên được tạo bởi các module. Chúng ta sẽ thảo luận về module trong Chương 7.

Nhiều khối impl

Mỗi struct được phép có nhiều khối impl. Ví dụ, Listing 5-15 tương đương với mã được hiển thị trong Listing 5-16, trong đó mỗi method nằm trong khối impl riêng của nó.

#[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: Viết lại Listing 5-15 bằng cách sử dụng nhiều khối impl

Không có lý do gì để tách các method này thành nhiều khối impl ở đây, nhưng đây là cú pháp hợp lệ. Chúng ta sẽ thấy một trường hợp mà nhiều khối impl hữu ích trong Chương 10, khi chúng ta thảo luận về các kiểu generic và trait.

Tóm tắt

Struct cho phép bạn tạo các kiểu tùy chỉnh có ý nghĩa cho lĩnh vực của bạn. Bằng cách sử dụng struct, bạn có thể giữ các mẩu dữ liệu liên quan kết nối với nhau và đặt tên cho từng mẩu để làm cho mã của bạn rõ ràng. Trong các khối impl, bạn có thể định nghĩa các hàm được liên kết với kiểu của bạn, và method là một loại hàm liên kết cho phép bạn chỉ định hành vi mà các thực thể của struct của bạn có.

Nhưng struct không phải là cách duy nhất bạn có thể tạo các kiểu tùy chỉnh: hãy chuyển sang tính năng enum của Rust để thêm một công cụ khác vào bộ công cụ của bạn.