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

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

Macro dbg! mà bạn có thể thấy trong code khác cũng dùng Debug.

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

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.

Cả hai đều tạo bản sao của một giá trị, nhưng theo cách khác nhau.

  • Clone cho 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ột String.
  • Copy tạ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" }
▶ Tự chạy thử

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 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: true
a != c: true
▶ Tự chạy thử

Vậ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 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: true
1.2.0
1.10.3
2.0.0
▶ Tự chạy thử

Vì 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 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: "" }
▶ Tự chạy thử

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 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°F
Thân nhiệt là 98.6°F
▶ Tự chạy thử

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

Trang Lỗi tự định nghĩa phát triển ý tưởng này chi tiết hơn.

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

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

Đâ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.

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 expansion
2 | 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 expansion
2 | 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.

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? true
Task { priority: 1, title: "Trả tiền nhà" }
Task { priority: 2, title: "Trả lời Sam" }
Task { priority: 3, title: "Tưới cây" }
▶ Tự chạy thử

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.50
Bánh ngọt: $4.05
Tổng cộng: $7.55
▶ Tự chạy thử
  • 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.
  • Clone sao chép khi được yêu cầu bằng .clone(). Copy sao chép tự động, nhưng chỉ cho kiểu đơn giản.
  • PartialEq/Eq cho bạn ==. PartialOrd/Ord cho bạn < và khả năng sắp xếp, so sánh các trường từ trên xuống dưới.
  • Default dựng một giá trị khởi đầu; ..Default::default() điền các trường bạn chưa đặt.
  • Cài đặt From là bạn có Into miễn phí. Toán tử ? dùng From để 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

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