Result trong Rust
Nếu Option biểu diễn một giá trị có thể bị thiếu, thì Result biểu diễn một thao tác có thể thất bại. Và khác với một giá trị bị thiếu, thất bại thường đi kèm một lý do.
Result trông như thế nào
Phần tiêu đề “Result trông như thế nào”Result là một enum khác trong thư viện chuẩn, lần này có hai biến thể đều mang dữ liệu:
enum Result<T, E> { Ok(T), // thành công, chứa một giá trị Err(E), // thất bại, chứa một lỗi}Một hàm có thể thất bại
Phần tiêu đề “Một hàm có thể thất bại”.parse(), mà bạn đã gặp ở trang Ép kiểu, là một ví dụ thực tế hay: chuyển văn bản thành số có thể thất bại nếu văn bản đó không thật sự là số:
fn main() { let good: Result<i32, std::num::ParseIntError> = "42".parse(); let bad: Result<i32, std::num::ParseIntError> = "abc".parse();
println!("{:?}", good); println!("{:?}", bad);}Kết quả:
Ok(42)Err(ParseIntError { kind: InvalidDigit })ParseIntError { kind: InvalidDigit } là cách Rust in lỗi bằng tiếng Anh: chuỗi có một ký tự không phải chữ số.
Xử lý Result bằng match
Phần tiêu đề “Xử lý Result bằng match”fn main() { let input = "42";
match input.parse::<i32>() { Ok(number) => println!("Đã chuyển đổi: {number}"), Err(e) => println!("Chuyển đổi thất bại: {e}"), }}Cùng hình dạng như khi xử lý Option bằng match, chỉ khác là nhánh Err cho bạn một giá trị lỗi thật sự để xem xét hoặc báo cáo, chứ không chỉ là “không có gì cả”.
Tự viết hàm trả về Result
Phần tiêu đề “Tự viết hàm trả về Result”fn divide(a: f64, b: f64) -> Result<f64, String> { if b == 0.0 { Err(String::from("không thể chia cho 0")) } else { Ok(a / b) }}
fn main() { match divide(10.0, 2.0) { Ok(result) => println!("Kết quả: {result}"), Err(e) => println!("Lỗi: {e}"), }
match divide(10.0, 0.0) { Ok(result) => println!("Kết quả: {result}"), Err(e) => println!("Lỗi: {e}"), }}Kết quả:
Kết quả: 5Lỗi: không thể chia cho 0Trả về Result<f64, String> buộc mọi nơi gọi phải tính đến khả năng thất bại: không có cách nào vô tình coi phép chia cho 0 như một con số bình thường.
Toán tử ?
Phần tiêu đề “Toán tử ?”Nối nhiều thao tác mà thao tác nào cũng có thể thất bại sẽ dẫn đến những khối match lồng nhau rất sâu. Toán tử ? làm phẳng chúng: nó có nghĩa là “nếu đây là Err, trả lỗi đó về ngay từ hàm hiện tại; nếu không, đưa tôi giá trị bên trong Ok”.
fn parse_and_double(input: &str) -> Result<i32, std::num::ParseIntError> { let number = input.parse::<i32>()?; // trả về sớm nếu chuyển đổi thất bại Ok(number * 2)}
fn main() { println!("{:?}", parse_and_double("21")); println!("{:?}", parse_and_double("oops"));}Kết quả:
Ok(42)Err(ParseIntError { kind: InvalidDigit })Không có ?, dòng đó sẽ cần một match riêng chỉ để lấy number ra hoặc thoát sớm cùng lỗi. ? làm đúng việc đó chỉ bằng một ký tự. Nó chỉ dùng được bên trong một hàm cũng trả về Result (hoặc Option), vì đó là nơi nó có thể trả lỗi về.
Result hay panic!
Phần tiêu đề “Result hay panic!”Rust còn có panic!, thứ dừng chương trình ngay lập tức; trang tiếp theo sẽ nói kỹ hơn. Quy tắc nhanh: dùng Result cho những thất bại mà nơi gọi có thể muốn xử lý (đầu vào sai, file không tồn tại); để dành panic! cho tình huống cho thấy có lỗi thật sự trong chương trình, khi chạy tiếp còn tệ hơn là dừng lại.
Khi hàm của bạn có thể thất bại theo nhiều cách khác nhau, một lỗi String đơn thuần sẽ không còn đủ. Trang Lỗi tự định nghĩa chỉ cách định nghĩa kiểu lỗi của riêng bạn, và anyhow và thiserror giới thiệu các crate mà phần lớn dự án Rust dùng để bớt code lặp lại.
Luyện tập
Phần tiêu đề “Luyện tập”1. Kiểm tra tuổi
Phần tiêu đề “1. Kiểm tra tuổi”Hãy viết fn parse_age(input: &str) -> Result<u32, String>. Hàm phải trả về Err kèm thông báo dễ hiểu nếu văn bản không phải là số, hoặc nếu số lớn hơn 150. Thử với "34", "abc" và "200". Gợi ý: match trên input.parse::<u32>(), và trong nhánh Ok dùng if để kiểm tra độ lớn.
Xem lời giải
fn parse_age(input: &str) -> Result<u32, String> { match input.parse::<u32>() { Ok(age) => { if age > 150 { Err(format!("{age} tuổi thì không thể có thật")) } else { Ok(age) } } Err(_) => Err(format!("'{input}' không phải là số")), }}
fn main() { for input in ["34", "abc", "200"] { match parse_age(input) { Ok(age) => println!("Tuổi: {age}"), Err(e) => println!("Lỗi: {e}"), } }}Kết quả:
Tuổi: 34Lỗi: 'abc' không phải là sốLỗi: 200 tuổi thì không thể có thật2. Cộng hai số bằng ?
Phần tiêu đề “2. Cộng hai số bằng ?”Hãy viết fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> chuyển đổi cả hai chuỗi rồi trả về tổng. Dùng ? thay cho match. In kết quả cho ("10", "20") và ("10", "twenty"). Gợi ý: bạn cần một ? cho mỗi parse, và dòng cuối bọc tổng trong Ok(...).
Xem lời giải
fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> { let x = a.parse::<i32>()?; let y = b.parse::<i32>()?; Ok(x + y)}
fn main() { println!("{:?}", add_strings("10", "20")); println!("{:?}", add_strings("10", "twenty"));}Kết quả:
Ok(30)Err(ParseIntError { kind: InvalidDigit })Tóm tắt
Phần tiêu đề “Tóm tắt”Result<T, E>biểu diễn một thao tác có thể thành công (Ok) hoặc thất bại (Err) kèm một giá trị lỗi.matchxử lý tường minh cả hai trường hợp, cùng khuôn mẫu như vớiOption.- Toán tử
?tự động trảErrvề sớm, nên bạn không cần mộtmatchsau mỗi lời gọi có thể thất bại. - Ưu tiên
Resulthơnpanic!cho những lỗi mà nơi gọi có thể khắc phục được.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Hai biến thể của Result<T, E> là gì?
2. Toán tử ? làm gì bên trong một hàm trả về Result?
3. Theo trang này, khi nào nên ưu tiên Result hơn panic!?
4. Trong ví dụ divide trên trang này, divide(10.0, 0.0) trả về gì?
5. Vì sao toán tử ? chỉ dùng được bên trong hàm cũng trả về Result (hoặc Option)?
Điểm: 0 / 5
Đọc thêm
Phần tiêu đề “Đọc thêm”- Recoverable Errors with Result — The Rust Book (tiếng Anh): chương chính thức về
Result,unwrap,expectvà toán tử?, với ví dụ mở file. std::result— tài liệu thư viện chuẩn (tiếng Anh): tổng quan cách làm việc vớiResult, gồm các phương thức nhưmap_err,unwrap_orvàok.- Result — Rust by Example (tiếng Anh): ví dụ chạy được về xử lý
Resultvà trả nó về từmain. - The ? operator — Rust by Example (tiếng Anh): cái nhìn ngắn gọn, tập trung vào cách
?thay thế các khốimatchlồng nhau.