Các trait thông dụng trong Rust
Ở trang Trait, bạn đã tự viết trait của riêng mình. Nhưng thư viện chuẩn đã có sẵn một bộ trait mà gần như chương trình Rust nào cũng dùng. Chúng trả lời những câu hỏi đơn giản về một kiểu: In nó ra được không? Sao chép được không? So sánh được không? Tạo một giá trị rỗng được không?
Hãy hình dung chúng như những chiếc huy hiệu trên đồng phục. Mỗi huy hiệu nói “kiểu này biết làm X”. Nhiều huy hiệu bạn có miễn phí với #[derive(...)]. Số khác thì bạn phải tự viết. Trang này đi qua những trait bạn sẽ gặp nhiều nhất, và kết thúc bằng một bảng để bạn quay lại tra cứu.
Debug: in ra cho lập trình viên
Phần tiêu đề “Debug: in ra cho lập trình viên”Debug cho phép bạn in một giá trị bằng {:?}. Nó dành cho bạn, lập trình viên: để kiểm tra giá trị trong lúc viết và sửa code. Bạn gần như không bao giờ viết tay nó; chỉ cần derive. Dùng {:#?} để có bản “đẹp” với mỗi trường một dòng, dễ đọc hơn cho các struct lớn:
#[derive(Debug)]struct Book { title: String, pages: u32,}
fn main() { let book = Book { title: String::from("Dune"), pages: 412 }; println!("{:?}", book); println!("{:#?}", book);}Kết quả:
Book { title: "Dune", pages: 412 }Book { title: "Dune", pages: 412,}Macro dbg! mà bạn có thể thấy trong code khác cũng dùng Debug.
Display: in ra cho người dùng
Phần tiêu đề “Display: in ra cho người dùng”Display là thứ mà {} dùng. Nó dành cho văn bản hướng tới người dùng: thông báo mà một người đọc trên màn hình. Rust không thể đoán nó nên trông thế nào, nên không có derive. Bạn tự viết bằng cách cài đặt fmt::Display:
use std::fmt;
struct Book { title: String, author: String,}
impl fmt::Display for Book { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{} ({})", self.title, self.author) }}
fn main() { let book = Book { title: String::from("Dune"), author: String::from("Frank Herbert"), };
println!("{}", book);
let text = book.to_string(); println!("Chuỗi đó dài {} ký tự", text.len());}Kết quả:
Dune (Frank Herbert)Chuỗi đó dài 20 ký tựVăn bản được in chỉ dùng chữ không dấu vì len() đếm byte; với chữ tiếng Việt có dấu, số byte sẽ lớn hơn số ký tự.
write! hoạt động giống println!, nhưng ghi vào formatter f thay vì ra màn hình. Một phần thưởng nhỏ: khi một kiểu đã có Display, bạn được .to_string() miễn phí. Bạn sẽ gặp lại Display ở trang Lỗi tự định nghĩa, nơi nó cho lỗi một thông báo thân thiện.
Clone và Copy
Phần tiêu đề “Clone và Copy”Cả hai đều tạo bản sao của một giá trị, nhưng theo cách khác nhau.
Clonecho bạn phương thức.clone(). Bạn gọi nó có chủ ý, và nó có thể tốn công thật sự, như sao chép toàn bộ chữ trong mộtString.Copytạo bản sao tự động mỗi khi bạn gán hoặc truyền giá trị. Nó chỉ được phép cho những kiểu nhỏ, đơn giản, sao chép được từng bit: số,bool,char, và struct chỉ gồm những thứ đó.
#[derive(Debug, Clone, Copy)]struct Point { x: i32, y: i32,}
#[derive(Debug, Clone)]struct User { name: String,}
fn main() { let p1 = Point { x: 1, y: 2 }; let p2 = p1; // Copy: p1 vẫn dùng được println!("{:?} và {:?}", p1, p2);
let u1 = User { name: String::from("Ana") }; let u2 = u1.clone(); // Clone: ta yêu cầu một bản sao println!("{:?} và {:?}", u1, u2);}Kết quả:
Point { x: 1, y: 2 } và Point { x: 1, y: 2 }User { name: "Ana" } và User { name: "Ana" }Không có Copy, let p2 = p1; sẽ move p1, và bạn không dùng nó được nữa (xem Quyền sở hữu). Lưu ý Copy luôn cần cả Clone: đó là lý do bạn derive cả hai cùng nhau.
PartialEq và Eq: so sánh bằng ==
Phần tiêu đề “PartialEq và Eq: so sánh bằng ==”PartialEq cho phép bạn dùng == và !=. Khi bạn derive nó, hai giá trị bằng nhau nếu mọi trường của chúng bằng nhau:
#[derive(Debug, PartialEq)]struct Point { x: i32, y: i32,}
fn main() { let a = Point { x: 1, y: 2 }; let b = Point { x: 1, y: 2 }; let c = Point { x: 5, y: 0 };
println!("a == b: {}", a == b); println!("a != c: {}", a != c);}Kết quả:
a == b: truea != c: trueVậy Eq là gì? Nó không thêm phương thức mới nào. Nó là một lời hứa: “mọi giá trị luôn bằng chính nó”. Nghe hiển nhiên, nhưng số thực phá vỡ điều đó: trong Rust, f64::NAN == f64::NAN là false. Vì vậy f64 có PartialEq nhưng không có Eq. Bạn cần Eq (cùng với Hash) để dùng kiểu của mình làm khoá trong HashMap hoặc trong một HashSet. Nếu struct của bạn không có trường số thực, hãy derive cả hai.
PartialOrd và Ord: so sánh bằng < và >
Phần tiêu đề “PartialOrd và Ord: so sánh bằng < và >”PartialOrd cho phép bạn dùng <, >, <= và >=. Ord thêm một thứ tự đầy đủ, thứ mà .sort(), .max() và .min() cần. Khi bạn derive chúng, Rust so sánh các trường từ trên xuống dưới: trường đầu tiên trước, và chỉ khi chúng bằng nhau thì mới xét trường kế tiếp.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]struct Version { major: u32, minor: u32, patch: u32,}
fn main() { let old = Version { major: 1, minor: 2, patch: 0 }; let new = Version { major: 1, minor: 10, patch: 3 }; println!("cũ < mới: {}", old < new);
let mut versions = vec![ Version { major: 2, minor: 0, patch: 0 }, new, old, ]; versions.sort();
for v in &versions { println!("{}.{}.{}", v.major, v.minor, v.patch); }}Kết quả:
cũ < mới: true1.2.01.10.32.0.0Vì các trường được so sánh theo thứ tự, thứ tự bạn viết các trường là quan trọng. Ở đây major quan trọng nhất, đúng y như số phiên bản. Với enum, biến thể nào được liệt kê trước thì được coi là “nhỏ hơn”. Giống như với Eq, số thực chỉ có PartialOrd, không có Ord; trang Sắp xếp chỉ cách sắp xếp số thực dù vậy.
Default: một giá trị khởi đầu hợp lý
Phần tiêu đề “Default: một giá trị khởi đầu hợp lý”Default cho kiểu của bạn một hàm ::default() dựng nên giá trị “khởi đầu”. Khi được derive, mỗi trường nhận giá trị mặc định của riêng nó: 0 cho số, false cho bool, một String rỗng, một Vec rỗng. Cú pháp ..Default::default() cho phép bạn đặt vài trường và để phần còn lại tự điền:
#[derive(Debug, Default)]struct Settings { volume: u32, muted: bool, username: String,}
fn main() { let basic = Settings::default(); println!("{:?}", basic);
let custom = Settings { volume: 80, ..Default::default() }; println!("{:?}", custom);}Kết quả:
Settings { volume: 0, muted: false, username: "" }Settings { volume: 80, muted: false, username: "" }Nếu bạn muốn giá trị khởi đầu khác (chẳng hạn âm lượng 50), hãy bỏ derive và tự viết impl Default for Settings, với một fn default() -> Self trả về các giá trị bạn muốn.
From và Into: chuyển đổi giữa các kiểu
Phần tiêu đề “From và Into: chuyển đổi giữa các kiểu”From mô tả cách dựng một kiểu từ kiểu khác. Bạn đã dùng nó rồi: String::from("hi"). Khi bạn cài đặt From, Rust cho bạn Into miễn phí, theo chiều ngược lại:
struct Celsius(f64);struct Fahrenheit(f64);
impl From<Celsius> for Fahrenheit { fn from(c: Celsius) -> Self { Fahrenheit(c.0 * 9.0 / 5.0 + 32.0) }}
fn main() { let boiling = Fahrenheit::from(Celsius(100.0)); println!("Nước sôi ở {}°F", boiling.0);
let body: Fahrenheit = Celsius(37.0).into(); println!("Thân nhiệt là {:.1}°F", body.0);}Kết quả:
Nước sôi ở 212°FThân nhiệt là 98.6°FVới .into(), Rust cần biết kiểu đích, đó là lý do chúng ta viết let body: Fahrenheit.
Cách ? dùng From. Đây là chỗ From thật sự toả sáng. Khi toán tử ? gặp một lỗi, nó gọi From::from để biến lỗi đó thành kiểu lỗi mà hàm của bạn trả về. Vì vậy nếu bạn cài đặt From cho kiểu lỗi của riêng mình, ? sẽ chuyển đổi lỗi giúp bạn:
use std::num::ParseIntError;
#[derive(Debug)]enum AppError { BadNumber(ParseIntError),}
impl From<ParseIntError> for AppError { fn from(e: ParseIntError) -> Self { AppError::BadNumber(e) }}
fn parse_age(text: &str) -> Result<u32, AppError> { let age: u32 = text.parse()?; // ParseIntError trở thành AppError ở đây Ok(age)}
fn main() { println!("{:?}", parse_age("42")); println!("{:?}", parse_age("abc"));}Kết quả:
Ok(42)Err(BadNumber(ParseIntError { kind: InvalidDigit }))Trang Lỗi tự định nghĩa phát triển ý tưởng này chi tiết hơn.
Nạp chồng toán tử với Add
Phần tiêu đề “Nạp chồng toán tử với Add”Trong Rust, a + b thật ra là lời gọi phương thức add của trait std::ops::Add. Cài đặt nó cho kiểu của bạn, và + sẽ chạy được. type Output cho biết phép cộng tạo ra kiểu gì:
use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]struct Point { x: i32, y: i32,}
impl Add for Point { type Output = Point;
fn add(self, other: Point) -> Point { Point { x: self.x + other.x, y: self.y + other.y } }}
fn main() { let a = Point { x: 1, y: 2 }; let b = Point { x: 3, y: 4 }; println!("{:?}", a + b);}Kết quả:
Point { x: 4, y: 6 }Ý tưởng tương tự áp dụng cho Sub (-), Mul (*), Neg (dấu - một ngôi) và AddAssign (+=). Hãy dùng khi ý nghĩa là hiển nhiên: cộng hai điểm, hay cộng hai khoản tiền. Nếu người đọc phải đoán + nghĩa là gì với kiểu của bạn, thì một phương thức bình thường có tên rõ ràng sẽ tốt hơn.
Nên derive những trait nào?
Phần tiêu đề “Nên derive những trait nào?”Đây là hướng dẫn nhanh cho một struct điển hình:
| Trait | Derive? | Bạn nhận được gì |
|---|---|---|
Debug |
Hầu như luôn luôn | In bằng {:?} và {:#?} |
Clone |
Thường là có | .clone() |
Copy |
Chỉ cho kiểu nhỏ, không có trường String, Vec hay Box |
Tự động sao chép thay vì move |
PartialEq |
Thường là có | == và != |
Eq |
Khi không có trường số thực | Cần cho khoá HashMap (cùng với Hash) |
Hash |
Khi dùng làm khoá HashMap hoặc trong HashSet |
Băm (hashing) |
PartialOrd, Ord |
Khi giá trị có thứ tự tự nhiên | <, >, .sort(), .max() |
Default |
Khi một giá trị “rỗng” có ý nghĩa | ::default() và ..Default::default() |
Display |
Không derive được, phải viết tay | In bằng {} và .to_string() |
Một dòng khởi đầu rất phổ biến là #[derive(Debug, Clone, PartialEq)]. Thêm trait khác khi bạn cần: trình biên dịch sẽ báo khi thiếu trait nào.
Lỗi thường gặp
Phần tiêu đề “Lỗi thường gặp”Derive Copy cho struct có String.
#[derive(Clone, Copy)]struct User { name: String, age: u32,}error[E0204]: the trait `Copy` cannot be implemented for this type --> src/main.rs:2:8 |1 | #[derive(Clone, Copy)] | ---- in this derive macro expansion2 | struct User { | ^^^^3 | name: String, | ------------ this field does not implement `Copy`Rustc báo rằng không thể cài đặt Copy cho kiểu này, vì trường name không cài đặt Copy. Một String sở hữu bộ nhớ trên heap, nên không sao chép từng bit được. Hãy bỏ Copy và dùng .clone() khi cần một bản sao.
Derive Eq cho struct có số thực.
#[derive(Debug, PartialEq, Eq)]struct Product { name: String, price: f64 }error[E0277]: the trait bound `f64: Eq` is not satisfied --> src/main.rs:2:32 |1 | #[derive(Debug, PartialEq, Eq)] | -- in this derive macro expansion2 | struct Product { name: String, price: f64 } | ^^^^^^^^^^ the trait `Eq` is not implemented for `f64`Ở đây rustc báo f64 không cài đặt Eq. Hãy giữ PartialEq và bỏ Eq. Hoặc lưu giá bằng số xu nguyên trong một u64, cách này cũng an toàn hơn khi làm việc với tiền.
Luyện tập
Phần tiêu đề “Luyện tập”1. Sắp xếp danh sách việc cần làm
Phần tiêu đề “1. Sắp xếp danh sách việc cần làm”Hãy tạo struct Task có priority: u8 và title: String. Derive các trait cần thiết để bạn sắp xếp được một Vec<Task>, in từng việc bằng {:?}, và so sánh hai việc bằng ==. Số càng nhỏ càng gấp, nên chúng phải đứng trước. Gợi ý: .sort() cần Ord, và Ord cũng cần Eq, PartialEq và PartialOrd. Đặt priority lên đầu để nó được so sánh trước.
Xem lời giải
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]struct Task { priority: u8, title: String,}
fn main() { let mut tasks = vec![ Task { priority: 3, title: String::from("Tưới cây") }, Task { priority: 1, title: String::from("Trả tiền nhà") }, Task { priority: 2, title: String::from("Trả lời Sam") }, ];
let copy = tasks[1].clone(); println!("Cùng một việc? {}", copy == tasks[1]);
tasks.sort(); for task in &tasks { println!("{:?}", task); }}Kết quả:
Cùng một việc? trueTask { priority: 1, title: "Trả tiền nhà" }Task { priority: 2, title: "Trả lời Sam" }Task { priority: 3, title: "Tưới cây" }2. Tiền biết in và biết cộng
Phần tiêu đề “2. Tiền biết in và biết cộng”Hãy tạo struct Money lưu số xu nguyên cents: u64. Cài đặt From<u64> để 350.into() chạy được, Display để nó in ra dạng $3.50, và Add để hai khoản tiền cộng được bằng +. Gợi ý: số đô la là cents / 100 và phần còn lại là cents % 100. In phần còn lại bằng {:02} để 5 xu hiện thành 05. Derive Copy để bạn vẫn in được các khoản tiền sau khi cộng.
Xem lời giải
use std::fmt;use std::ops::Add;
#[derive(Debug, Clone, Copy, PartialEq)]struct Money { cents: u64,}
impl From<u64> for Money { fn from(cents: u64) -> Self { Money { cents } }}
impl fmt::Display for Money { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "${}.{:02}", self.cents / 100, self.cents % 100) }}
impl Add for Money { type Output = Money;
fn add(self, other: Money) -> Money { Money { cents: self.cents + other.cents } }}
fn main() { let coffee: Money = 350.into(); let cake = Money::from(405); let total = coffee + cake;
println!("Cà phê: {coffee}"); println!("Bánh ngọt: {cake}"); println!("Tổng cộng: {total}");}Kết quả:
Cà phê: $3.50Bánh ngọt: $4.05Tổng cộng: $7.55Tóm tắt
Phần tiêu đề “Tóm tắt”Debug({:?}) dành cho lập trình viên và hầu như luôn được derive.Display({}) dành cho người dùng và được viết tay.Clonesao chép khi được yêu cầu bằng.clone().Copysao chép tự động, nhưng chỉ cho kiểu đơn giản.PartialEq/Eqcho bạn==.PartialOrd/Ordcho bạn<và khả năng sắp xếp, so sánh các trường từ trên xuống dưới.Defaultdựng một giá trị khởi đầu;..Default::default()điền các trường bạn chưa đặt.- Cài đặt
Fromlà bạn cóIntomiễn phí. Toán tử?dùngFromđể chuyển đổi lỗi. - Cài đặt
std::ops::Add(và các trait cùng họ) để+chạy được với kiểu của riêng bạn.
Muốn đi xa hơn? Trait nâng cao giải thích kiểu liên kết (associated type) như type Output, tham số kiểu mặc định, và cách cài đặt trait cho một kiểu không phải do bạn viết.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. {} trong println! dùng trait nào?
2. Vì sao một struct có trường String không derive được Copy?
3. Khi bạn derive PartialOrd, hai struct được so sánh thế nào?
4. Bạn đã cài đặt From<ParseIntError> for AppError. Điều đó cho phép ? làm gì?
5. Bạn cài đặt trait nào để a + b chạy được với kiểu của riêng mình?
Điểm: 0 / 5
Đọc thêm
Phần tiêu đề “Đọc thêm”- Derivable Traits — The Rust Book (Appendix C) (tiếng Anh): mọi trait bạn có thể
#[derive], bản derive làm gì, và khi nào bạn cần nó. std::fmt— formatting and Display (tiếng Anh): mọi tuỳ chọn định dạng như độ rộng, padding và{:#?}, cùng vị trí củaDisplayvàDebug.FromandInto— Rust by Example (tiếng Anh): ví dụ ngắn chạy được về chuyển đổi giữa các kiểu của riêng bạn.std::ops— operator traits (tiếng Anh): danh sách đầy đủ các toán tử bạn có thể nạp chồng, từAddđếnIndex.std::cmp— comparison traits (tiếng Anh):PartialEq,Eq,PartialOrdvàOrdchi tiết, cùng các quy tắc mà mỗi trait hứa hẹn.