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

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 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
}

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

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

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

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ả”.

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ả: 5
Lỗi: không thể chia cho 0
▶ Tự chạy thử

Trả 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.

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

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

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.

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: 34
Lỗi: 'abc' không phải là số
Lỗi: 200 tuổi thì không thể có thật
▶ Tự chạy thử

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ự chạy thử
  • 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.
  • match xử lý tường minh cả hai trường hợp, cùng khuôn mẫu như với Option.
  • Toán tử ? tự động trả Err về sớm, nên bạn không cần một match sau mỗi lời gọi có thể thất bại.
  • Ưu tiên Result hơn panic! cho những lỗi mà nơi gọi có thể khắc phục được.

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