Kiểu lỗi tự định nghĩa trong Rust
Ở trang Result, mọi lỗi đều chỉ là một String. Với ví dụ nhỏ thì ổn, nhưng nó bắt đầu lộ điểm yếu khi một hàm có thể thất bại theo nhiều cách khác nhau. Không thể match trên một String, nên nơi gọi không biết được loại thất bại nào đã xảy ra, trừ khi tự phân tích nội dung chữ.
Vấn đề của lỗi dạng String
Phần tiêu đề “Vấn đề của lỗi dạng String”fn withdraw(balance: f64, amount: f64) -> Result<f64, String> { if amount > balance { return Err(String::from("không đủ tiền")); } if amount < 0.0 { return Err(String::from("số tiền không được âm")); } Ok(balance - amount)}Nếu nơi gọi muốn phản ứng khác nhau với “không đủ tiền” và “số tiền âm”, cách duy nhất là so sánh chính chuỗi lỗi. Cách đó rất mong manh, và hỏng ngay khi bạn đổi cách diễn đạt thông báo.
Định nghĩa lỗi bằng enum
Phần tiêu đề “Định nghĩa lỗi bằng enum”Một enum giải quyết chuyện này: mỗi loại thất bại trở thành một biến thể riêng, và nơi gọi có thể match trực tiếp trên nó:
enum WithdrawError { InsufficientFunds, NegativeAmount,}
fn withdraw(balance: f64, amount: f64) -> Result<f64, WithdrawError> { if amount < 0.0 { return Err(WithdrawError::NegativeAmount); } if amount > balance { return Err(WithdrawError::InsufficientFunds); } Ok(balance - amount)}
fn main() { match withdraw(100.0, 150.0) { Ok(remaining) => println!("Số dư mới: {remaining}"), Err(WithdrawError::InsufficientFunds) => println!("Không đủ tiền"), Err(WithdrawError::NegativeAmount) => println!("Không thể rút một số tiền âm"), }}Kết quả:
Không đủ tiềnGiờ trình biên dịch buộc mọi loại thất bại đều phải được xử lý: cùng sự đảm bảo mà match luôn mang lại với enum, chỉ là áp dụng cho lỗi.
Cài đặt Display để có thông báo lỗi tử tế
Phần tiêu đề “Cài đặt Display để có thông báo lỗi tử tế”Thử in WithdrawError bằng {} ngay bây giờ, bạn sẽ thấy nó không biên dịch: Rust không biết cách biến nó thành chữ. Cài đặt trait Display sẽ giải quyết việc đó. Trait sẽ có trang riêng ngay sau đây (Trait và Các trait thông dụng). Tạm thời, bạn cứ sao chép nguyên khuôn mẫu này:
use std::fmt;
enum WithdrawError { InsufficientFunds, NegativeAmount,}
impl fmt::Display for WithdrawError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { WithdrawError::InsufficientFunds => write!(f, "không đủ tiền"), WithdrawError::NegativeAmount => write!(f, "số tiền không được âm"), } }}
fn main() { let error = WithdrawError::InsufficientFunds; println!("Lỗi: {error}");}Kết quả:
Lỗi: không đủ tiềnGiờ WithdrawError hoạt động như mọi lỗi khác mà bạn muốn in cho người dùng hoặc ghi vào log, trong khi vẫn match được đầy đủ cho những đoạn code cần phản ứng với một trường hợp cụ thể.
Khi nào một crate giúp được bạn
Phần tiêu đề “Khi nào một crate giúp được bạn”Tự tay viết Display và đống code lặp lại quanh một kiểu lỗi tự định nghĩa sẽ nhanh chóng nhàm chán trong một dự án thật. Crate thiserror (tiếng Anh) sinh phần lớn code đó cho bạn bằng một #[derive], còn anyhow (tiếng Anh) được ưa chuộng cho code ứng dụng chỉ cần truyền lỗi đi mà không phải định nghĩa kiểu cho từng lỗi. Nên biết chúng tồn tại, nhưng bạn không cần chúng để học phần nền tảng. Khi sẵn sàng, trang anyhow và thiserror sẽ cho bạn thấy cả hai hoạt động ra sao, và khi nào nên chọn cái nào.
Luyện tập
Phần tiêu đề “Luyện tập”1. Lỗi đăng nhập
Phần tiêu đề “1. Lỗi đăng nhập”Hãy định nghĩa enum LoginError với hai biến thể EmptyUsername và PasswordTooShort, rồi cài đặt Display cho nó. Sau đó viết fn check_login(user: &str, password: &str) -> Result<String, LoginError> trả về Ok kèm lời chào, hoặc lỗi tương ứng. Mật khẩu phải có ít nhất 8 ký tự. Gợi ý: .is_empty() và .len() trên &str cho bạn đủ những gì cần kiểm tra.
Xem lời giải
use std::fmt;
enum LoginError { EmptyUsername, PasswordTooShort,}
impl fmt::Display for LoginError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { LoginError::EmptyUsername => write!(f, "tên người dùng không được để trống"), LoginError::PasswordTooShort => write!(f, "mật khẩu phải có ít nhất 8 ký tự"), } }}
fn check_login(user: &str, password: &str) -> Result<String, LoginError> { if user.is_empty() { return Err(LoginError::EmptyUsername); } if password.len() < 8 { return Err(LoginError::PasswordTooShort); } Ok(format!("Chào mừng, {user}!"))}
fn main() { let attempts = [("ana", "correct-horse"), ("", "secret123"), ("ben", "short")];
for (user, password) in attempts { match check_login(user, password) { Ok(message) => println!("{message}"), Err(e) => println!("Lỗi: {e}"), } }}Kết quả:
Chào mừng, ana!Lỗi: tên người dùng không được để trốngLỗi: mật khẩu phải có ít nhất 8 ký tựCác mật khẩu mẫu được giữ tiếng Anh vì .len() đếm byte, mà chữ có dấu chiếm nhiều byte hơn.
2. Lỗi mang theo chi tiết
Phần tiêu đề “2. Lỗi mang theo chi tiết”Hãy viết fn parse_percent(input: &str) -> Result<u32, PercentError>, trong đó PercentError có hai biến thể chứa dữ liệu: NotANumber(String) (đoạn chữ sai) và TooLarge(u32) (con số vượt quá 100). Cho thông báo Display chứa luôn những chi tiết đó. Gợi ý: biến thể của enum có thể chứa giá trị, như ở trang Enum. Trong fmt, hãy match với PercentError::TooLarge(n) => ... để lấy giá trị ra.
Xem lời giải
use std::fmt;
enum PercentError { NotANumber(String), TooLarge(u32),}
impl fmt::Display for PercentError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { PercentError::NotANumber(text) => write!(f, "'{text}' không phải là số"), PercentError::TooLarge(n) => write!(f, "{n} lớn hơn 100"), } }}
fn parse_percent(input: &str) -> Result<u32, PercentError> { match input.parse::<u32>() { Ok(n) => { if n > 100 { Err(PercentError::TooLarge(n)) } else { Ok(n) } } Err(_) => Err(PercentError::NotANumber(input.to_string())), }}
fn main() { for input in ["75", "một nửa", "250"] { match parse_percent(input) { Ok(n) => println!("{n}%"), Err(e) => println!("Lỗi: {e}"), } }}Kết quả:
75%Lỗi: 'một nửa' không phải là sốLỗi: 250 lớn hơn 100Tóm tắt
Phần tiêu đề “Tóm tắt”- Lỗi dạng
Stringdùng được cho ví dụ nhỏ, nhưng nơi gọi khôngmatchriêng từng loại được. - Hãy định nghĩa một enum với mỗi biến thể cho một loại thất bại, để nơi gọi
matchđược trên lỗi cụ thể. - Cài đặt
Display(impl fmt::Display for YourError) để kiểu lỗi in được bằng{}. - Trong dự án lớn hơn, các crate như
thiserrorvàanyhowloại bỏ phần lớn code lặp lại.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Vì sao kiểu lỗi String không còn đủ khi một hàm có thể thất bại theo nhiều cách?
2. Điều gì giúp nơi gọi withdraw phản ứng khác nhau với InsufficientFunds và NegativeAmount?
3. Cài đặt Display cho một kiểu lỗi tự định nghĩa cho phép bạn làm gì?
4. Các crate như thiserror và anyhow giúp được gì?
5. Vì sao so sánh trực tiếp chuỗi thông báo lỗi bị coi là mong manh?
Điểm: 0 / 5
Đọc thêm
Phần tiêu đề “Đọc thêm”- Defining an error type — Rust by Example (tiếng Anh): một ví dụ ngắn, chạy được, về kiểu lỗi tự định nghĩa có
Display. - Wrapping errors — Rust by Example (tiếng Anh): bước tiếp theo, một enum lỗi bọc các lỗi khác và dùng được với
?thông quaFrom. std::error::Error— tài liệu std (tiếng Anh): trait chuẩn mà các kiểu lỗi “thật sự” cài đặt, và phương thứcsource()của nó dùng để làm gì.- To panic! or Not to panic! — The Rust Book (tiếng Anh): hướng dẫn khi nào nên trả về lỗi và khi nào nên dừng chương trình.
- thiserror — docs.rs (tiếng Anh): crate tự derive
DisplayvàErrorcho các enum lỗi của bạn, nhờ vậy bạn viết ít code hơn nhiều.