Trait trong Rust
Một trait định nghĩa một tập hành vi mà một kiểu có thể cài đặt, hơi giống interface trong các ngôn ngữ khác. Đó là cách Rust nói “kiểu này làm được X”, và cũng là thứ đứng sau các ràng buộc trait (trait bound) mà bạn đã thấy ở trang Generic.
Định nghĩa một trait
Phần tiêu đề “Định nghĩa một trait”trait Speak { fn speak(&self) -> String;}Đoạn này nói: kiểu nào cài đặt Speak thì phải cung cấp một phương thức speak trả về String. Tự bản thân trait chỉ là một lời hứa: nó không làm gì cho đến khi có kiểu nào đó cài đặt nó.
Cài đặt một trait
Phần tiêu đề “Cài đặt một trait”trait Speak { fn speak(&self) -> String;}
struct Dog;struct Cat;
impl Speak for Dog { fn speak(&self) -> String { String::from("Gâu gâu!") }}
impl Speak for Cat { fn speak(&self) -> String { String::from("Meo meo!") }}
fn main() { let dog = Dog; let cat = Cat;
println!("{}", dog.speak()); println!("{}", cat.speak());}Kết quả:
Gâu gâu!Meo meo!Dog và Cat là hai kiểu không liên quan gì nhau, nhưng cả hai đều cài đặt Speak, nên cả hai đều gọi được .speak(). Đây cũng là khuôn mẫu của các khối impl ở trang Struct: impl Speak for Dog chỉ đơn giản nói “đây là cách Dog thực hiện trait Speak”.
Trait làm tham số hàm
Phần tiêu đề “Trait làm tham số hàm”Lợi ích thật sự nằm ở đây: bạn có thể viết một hàm nhận bất kỳ kiểu nào cài đặt một trait, mà không cần quan tâm đó là kiểu cụ thể nào:
trait Speak { fn speak(&self) -> String;}
struct Dog;impl Speak for Dog { fn speak(&self) -> String { String::from("Gâu gâu!") }}
fn announce(animal: &impl Speak) { println!("Con vật nói: {}", animal.speak());}
fn main() { let dog = Dog; announce(&dog);}animal: &impl Speak nghĩa là “bất kỳ kiểu nào cài đặt Speak, tôi không cần biết là kiểu nào”. Thêm một kiểu Bird cũng cài đặt Speak, và announce dùng được với nó luôn mà không phải sửa gì.
Những trait bạn đã dùng rồi
Phần tiêu đề “Những trait bạn đã dùng rồi”Còn nhớ #[derive(Debug)] ở trang Struct không? Bản thân Debug là một trait: derive chỉ tự sinh phần cài đặt giúp bạn thay vì bạn phải viết tay. Vài trait thông dụng khác bạn sẽ gặp: Clone (cho .clone()), PartialEq (cho ==), và PartialOrd (cho <, >, thứ bạn đã thấy làm ràng buộc trait ở trang Generic).
Cài đặt mặc định
Phần tiêu đề “Cài đặt mặc định”Một trait có thể cung cấp sẵn thân phương thức mặc định, và các kiểu cài đặt có thể dùng nguyên hoặc ghi đè (override):
trait Greet { fn name(&self) -> String;
fn greet(&self) -> String { format!("Xin chào, {}!", self.name()) }}Kiểu nào cài đặt Greet chỉ phải định nghĩa name(): greet() có sẵn miễn phí, trừ khi kiểu đó chọn ghi đè nó.
Trait còn nhiều điều hơn những gì trang này nói. Trang Các trait thông dụng đi qua những trait chuẩn bạn sẽ dùng hằng ngày, còn Trait nâng cao nói về kiểu liên kết (associated type), supertrait và các công cụ khác mà bạn sẽ gặp trong những codebase lớn hơn.
Luyện tập
Phần tiêu đề “Luyện tập”1. Hình có diện tích
Phần tiêu đề “1. Hình có diện tích”Hãy định nghĩa trait Area với một phương thức fn area(&self) -> f64. Cài đặt nó cho Rectangle (có width và height) và Square (có side). In diện tích của hình chữ nhật 3 x 4 và hình vuông cạnh 5. Gợi ý: mỗi khối impl Area for ... chỉ cần một phương thức, và thân của nó chỉ là một phép nhân.
Xem lời giải
trait Area { fn area(&self) -> f64;}
struct Rectangle { width: f64, height: f64,}
struct Square { side: f64,}
impl Area for Rectangle { fn area(&self) -> f64 { self.width * self.height }}
impl Area for Square { fn area(&self) -> f64 { self.side * self.side }}
fn main() { let rect = Rectangle { width: 3.0, height: 4.0 }; let square = Square { side: 5.0 };
println!("Hình chữ nhật: {}", rect.area()); println!("Hình vuông: {}", square.area());}Kết quả:
Hình chữ nhật: 12Hình vuông: 252. Một hàm cho nhiều kiểu
Phần tiêu đề “2. Một hàm cho nhiều kiểu”Hãy định nghĩa trait Describe với fn describe(&self) -> String. Cài đặt nó cho Book (có title) và Movie (có title và minutes). Rồi viết một hàm fn show(item: &impl Describe) in ra phần mô tả, và gọi nó với cả hai. Gợi ý: dùng format! để dựng từng chuỗi mô tả.
Xem lời giải
trait Describe { fn describe(&self) -> String;}
struct Book { title: String,}
struct Movie { title: String, minutes: u32,}
impl Describe for Book { fn describe(&self) -> String { format!("Sách: {}", self.title) }}
impl Describe for Movie { fn describe(&self) -> String { format!("Phim: {} ({} phút)", self.title, self.minutes) }}
fn show(item: &impl Describe) { println!("{}", item.describe());}
fn main() { let book = Book { title: String::from("Dế Mèn phiêu lưu ký") }; let movie = Movie { title: String::from("Up"), minutes: 96 };
show(&book); show(&movie);}Kết quả:
Sách: Dế Mèn phiêu lưu kýPhim: Up (96 phút)3. Dùng hoặc ghi đè cài đặt mặc định
Phần tiêu đề “3. Dùng hoặc ghi đè cài đặt mặc định”Lấy trait Greet trên trang này, với phương thức mặc định greet(). Cài đặt nó cho Person, giữ nguyên mặc định, và cho Pirate, ghi đè greet() để nói Ahoy, ...! (lời chào của cướp biển). Gợi ý: cả hai kiểu đều phải định nghĩa name(). Chỉ Pirate viết thêm greet() của riêng nó.
Xem lời giải
trait Greet { fn name(&self) -> String;
fn greet(&self) -> String { format!("Xin chào, {}!", self.name()) }}
struct Person { name: String,}
struct Pirate { name: String,}
impl Greet for Person { fn name(&self) -> String { self.name.clone() }}
impl Greet for Pirate { fn name(&self) -> String { self.name.clone() }
fn greet(&self) -> String { format!("Ahoy, {}!", self.name()) }}
fn main() { let ana = Person { name: String::from("Ana") }; let jack = Pirate { name: String::from("Jack") };
println!("{}", ana.greet()); println!("{}", jack.greet());}Kết quả:
Xin chào, Ana!Ahoy, Jack!Tóm tắt
Phần tiêu đề “Tóm tắt”- Trait định nghĩa hành vi mà một kiểu có thể cài đặt, giống interface.
impl TraitName for Typecung cấp phần cài đặt.&impl TraitNamelàm kiểu tham số sẽ nhận mọi kiểu cài đặt trait đó.- Trait có thể cung cấp thân phương thức mặc định mà kiểu cài đặt dùng nguyên hoặc ghi đè.
Debug,ClonevàPartialEqlà các trait thông dụng trong thư viện chuẩn:#[derive(...)]cài đặt chúng tự động.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Một trait định nghĩa điều gì?
2. animal: &impl Speak làm kiểu tham số có nghĩa là gì?
3. #[derive(Debug)] thật ra đang làm gì?
4. Cài đặt mặc định trong một trait là gì?
5. Theo trang này, cái nào sau đây là trait chứ không phải struct hay enum?
Điểm: 0 / 5
Đọc thêm
Phần tiêu đề “Đọc thêm”- Traits: Defining Shared Behavior — The Rust Book (tiếng Anh): chương chính thức về định nghĩa trait, phương thức mặc định, tham số
impl Traitvà ràng buộc trait. - Traits — Rust by Example (tiếng Anh): một ví dụ chạy được với trait, phương thức mặc định và phần cài đặt.
- Derive — Rust by Example (tiếng Anh): những trait chuẩn nào
#[derive(...)]cài đặt được giúp bạn, kèm ví dụ. - Advanced Traits — The Rust Book (tiếng Anh): bước tiếp theo, với kiểu liên kết, supertrait và khuôn mẫu newtype.
- Traits — The Rust Reference (tiếng Anh): các quy tắc chính xác cho việc định nghĩa trait, khi bạn muốn biết đầy đủ chi tiết.