Bỏ qua để đến nội dung

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.

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ó.

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!
▶ Tự chạy thử

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”.

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);
}
▶ Tự chạy thử

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ì.

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).

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.

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: 12
Hình vuông: 25
▶ Tự chạy thử

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)
▶ Tự chạy thử

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ự chạy thử
  • Trait định nghĩa hành vi mà một kiểu có thể cài đặt, giống interface.
  • impl TraitName for Type cung cấp phần cài đặt.
  • &impl TraitName là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, Clone và PartialEq là 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

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