1. Giới thiệu
Traits là một khái niệm trong Rust, tương tự như interface ở các ngôn ngữ lập trình khác, mặc dù có số số điểm khác biệt.
Mỗi kiểu dữ liệu sẽ có một số phương thức được định nghĩa và chỉ biến thuộc kiểu dữ liệu đó mới có thể gọi. Nếu muốn chia sẻ một phương thức mà nhiều kiểu có thể dùng chung, tùy biến lại theo từng kiểu thì chúng ta sẽ cần dùng đến traits.
Ở đây chúng ta có 2 struct là NewArticle
và Tweet
có các trường dữ liệu khác nhau, cả 2 kiểu này đều đang cần một phương thức summarize
để tóm tắt dữ liệu.
pub struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String,
} pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool,
}
pub trait Summary { fn summarize(&self) -> String;
}
Chúng ta định nghĩa traits với từ khóa trait
, theo sau là Summary
(tên của trait). Hàm summarize
trả về kiểu String
. Cũng như các hàm trong interface ở 1 số ngôn ngữ khác, hàm trong traits kết thúc bởi dấu ;
và không có thân hàm. Bởi nó chỉ là nguyên mẫu để các kiểu dữ liệu khác implement mà thôi.
2. Implement Trait
Implement trait với kiểu dữ liệu
Giờ là lúc chúng ta triển khai phương thức summarize
trên 2 struct NewsArticle
và Tweet
.
// src/lib.rs
pub trait Summary { fn summarize(&self) -> String;
} pub struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String,
} impl Summary for NewsArticle { fn summarize(&self) -> String { // trả về chuỗi gồm 3 thông tin: headline, author và localtion format!("{}, by {} ({})", self.headline, self.author, self.location) }
} pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool,
} impl Summary for Tweet { fn summarize(&self) -> String { // trả về chuỗi gồm 2 thông tin: username và content format!("{}: {}", self.username, self.content) }
}
Implement một trait cho một kiểu dữ liệu cũng tương tự như cách implement một method thông thường cho kiểu dữ liệu đó. Chỉ khác chút là sau từ khóa impl
sẽ là tên của trait bạn muốn sử dụng rồi kèm theo từ khóa for
.
Bây giờ thư viện đã triển khai đặc điểm Tóm tắt trên NewsArticle và Tweet, người dùng của thùng có thể gọi các phương thức đặc điểm trên các phiên bản NewsArticle và Tweet giống như cách chúng ta gọi các phương thức thông thường. Sự khác biệt duy nhất là người dùng phải đưa đặc điểm vào phạm vi cũng như các loại. Dưới đây là một ví dụ về cách thùng nhị phân có thể sử dụng thùng thư viện tổng hợp của chúng tôi:
Chúng ta vừa triển khai việc implement Summary trait cho 2 kiểu NewsArticle
và Tweet
. Bây giờ, chúng ta có thể gọi phương thức summarize
từ các instance của NewsArticle
hay Tweet
như các phương thức thông thường. Tất nhiên là tùy theo instace đó thuộc kiểu NewsArticle
hay Tweet
mà hàm summarize
tương ứng sẽ được thực thi.
use aggregator::{Summary, Tweet}; fn main() { let tweet = Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, }; println!("1 new tweet: {}", tweet.summarize()); // 1 new tweet: horse_books: of course, as you probably already know, people
}
Implement mặc định (Default Implementations)
Đôi khi sẽ hữu ích khi có hành vi mặc định cho một số hoặc tất cả các phương thức trong một đặc điểm thay vì yêu cầu triển khai cho tất cả các phương thức trên mọi loại. Sau đó, khi chúng tôi triển khai đặc điểm trên một loại cụ thể, chúng tôi có thể giữ hoặc ghi đè hành vi mặc định của mỗi phương thức.
Đôi khi chúng ta sẽ cần một phương thức mặc định chung có các kiểu thay vì phải triển khai các phương thức trên từng kiểu khác nhau. Sau đó, nếu muốn thay đổi tùy theo kiểu dữ liệu, chúng ta có thể lựa chọn phương án ghi đè (override).
pub trait Summary { fn summarize(&self) -> String { String::from("(Read more...)") }
} pub struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String,
} // Sử dụng phương thức summarize mặc định của trait Summary
impl Summary for NewsArticle {} pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool,
} impl Summary for Tweet { // Ghi đè phương thức summarize mặc định fn summarize(&self) -> String { format!("{}: {}", self.username, self.content) }
}
use aggregator::{self, NewsArticle, Summary}; fn main() { let article = NewsArticle { headline: String::from("Penguins win the Stanley Cup Championship!"), location: String::from("Pittsburgh, PA, USA"), author: String::from("Iceburgh"), content: String::from( "The Pittsburgh Penguins once again are the best \ hockey team in the NHL.", ), }; println!("New article available! {}", article.summarize()); // New article available! (Read more...)
}
Phương thức mặc định có thể gọi các phương thức khác cùng trait với nó, ngay cả khi phương thức đó chỉ ở dạng nguyên mẫu (prototype). Tính năng này sẽ cung cấp nhiều thứ hữu ích, chúng ta cùng xem qua ví dụ dưới đây:
pub trait Summary { fn summarize_author(&self) -> String; // phương thức summarize gọi phương thức summarize_author để in ra kết quả fn summarize(&self) -> String { format!("(Read more from {}...)", self.summarize_author()) }
} pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool,
} impl Summary for Tweet { // Triển khai phương thức summarize_author in ra @ + tên tác giả fn summarize_author(&self) -> String { format!("@{}", self.username) }
}
use aggregator::{self, Summary, Tweet}; fn main() { let tweet = Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, }; println!("1 new tweet: {}", tweet.summarize()); // 1 new tweet: (Read more from @horse_ebooks...)
}
Thay vì trả về 1 chuỗi cố định như ở ví dụ trước, thì phương thức summarize
bằng cách gọi summarize_author
đã tùy biến được tên tác giả Tweet trong kết quả trả về
Trait làm tham số
Chúng ta đã biết cách xác định và triển khai trait ở phần trên. Ngoài ra, trait cũng có thể đóng vai trò làm tham số trong hàm khi được định nghĩa với từ khóa impl
như sau:
pub fn notify(item: &impl Summary) { println!("Breaking news! {}", item.summarize());
}
Thay vì định nghĩa kiểu dữ liệu struct cần truyền vào NewArticle
hay Tweet
chẳng hạn, ta sẽ thay bằng &impl
. Tham số đầu vào của hàm lúc đó sẽ nhận tất cả kiểu dữ liệu đã implement trait Summary. Nếu truyền vào 1 kiểu dữ liệu chưa implement trait, trình biên dịch sẽ báo lỗi.
pub trait Summary { fn summarize(&self) -> String;
} pub struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String,
} impl Summary for NewsArticle { fn summarize(&self) -> String { format!("{}, by {} ({})", self.headline, self.author, self.location) }
} pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool,
} impl Summary for Tweet { fn summarize(&self) -> String { format!("{}: {}", self.username, self.content) }
} pub fn notify(item: &impl Summary) { println!("Breaking news! {}", item.summarize());
} fn main() { let tweet = Tweet { username: String::from("Pool and Billiards for Dummies"), content: String::from( "Billards Pool", ), reply: false, retweet: false, }; let article = NewsArticle { headline: String::from("Penguins win the Stanley Cup Championship!"), location: String::from("Pittsburgh, PA, USA"), author: String::from("Iceburgh"), content: String::from( "The Pittsburgh Penguins once again are the best \ hockey team in the NHL.", ), }; notify(&tweet); // Breaking news! Pool and Billiards for Dummies: Billards Pool notify(&article); // Breaking news! Penguins win the Stanley Cup Championship!, by Iceburgh (Pittsburgh, PA, USA)
}
Trait Bound Syntax
Ngày cách định nghĩa trait là tham số của hàm như trên, chúng ta có thể sử dụng cú pháp theo kiểu generic như sau:
pub fn notify<T: Summary>(item: &T) { println!("Breaking news! {}", item.summarize());
}
Trong trường hợp hàm có nhiều tham số trait, các này sẽ giống code trông ngắn gọn hơn hẳn
pub fn notify(item1: &impl Summary, item2: &impl Summary) { } pub fn notify<T: Summary>(item1: &T, item2: &T) { }
<T: Summary>
ám chỉ rằng T chỉ nhận các kiểu dữ liệu cho implement trait Summary
Sử dụng toán tử +
Khi muốn tham số đầu vào là 1 kiểu dữ liệu implement cùng lúc nhiều trait thì sao nhỉ 🤔 ? Chúng ta sẽ sử dụng đến phép toán +
pub fn notify(item: &(impl Summary + Display)) { } // hoặc pub fn notify<T: Summary + Display>(item: &T) { }
Kiểu dữ liệu truyền vào yêu cầu phải implement cả 2 trait
Sử dụng từ khóa where
Khi định nghĩa nhiều kiểu dữ liệu generic cộng với mỗi kiểu lại cần implement nhiều trait thì cú pháp định nghĩa sử dụng dấu + có phần khá cồng kềnh khi kéo dài lình thình dòng định nghĩa hàm.
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
Thay vào đó, chúng ta có thể sử dụng từ khóa where, phần định nghĩa các trait cần thiết sẽ được đưa xuống dòng thứ 2, giúp code trông tường mình, gọn gàng hơn ở ví dụ trên.
fn some_function<T, U>(t: &T, u: &U) -> i32 where T: Display + Clone, U: Clone + Debug
{
3. Trả về kiểu trait
Trait có thể được định nghĩa là kiểu trả về trong 1 hàm, chúng ta cùng xem ví dụ dưới đây.
fn returns_summarizable() -> impl Summary { Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, }
}
Tất nhiên kiểu dữ liệu trả về cần phải implement trait Summary
Tuy nhiên, bạn chỉ có thể sử dụng Impl Trait nếu bạn đang trả lại một kiểu duy nhất. Ví dụ dưới đây trả về NewsArticle
hoặc Tweet
sẽ bị báo lỗi
fn returns_summarizable(switch: bool) -> impl Summary { if switch { NewsArticle { headline: String::from( "Penguins win the Stanley Cup Championship!", ), location: String::from("Pittsburgh, PA, USA"), author: String::from("Iceburgh"), content: String::from( "The Pittsburgh Penguins once again are the best \ hockey team in the NHL.", ), } } else { Tweet { username: String::from("horse_ebooks"), content: String::from( "of course, as you probably already know, people", ), reply: false, retweet: false, } }
}