Panic trong Rust
Panic là cách Rust dừng chương trình ngay lập tức khi có chuyện hỏng tới mức không cứu vãn được. Nếu đã theo các trang trước, thật ra bạn đã vô tình gây ra vài lần panic rồi: truy cập chỉ số vượt quá cuối mảng, hay gọi .unwrap() trên một None, đều gây panic.
Tự gây ra panic
Phần tiêu đề “Tự gây ra panic”fn main() { panic!("có chuyện rất tệ đã xảy ra");}thread 'main' (12345) panicked at src/main.rs:2:5:có chuyện rất tệ đã xảy ranote: run with `RUST_BACKTRACE=1` environment variable to display a backtracePhần thông báo do Rust in ra bằng tiếng Anh: luồng main đã panic tại dòng 2, cột 5, và bạn có thể đặt biến môi trường RUST_BACKTRACE=1 để xem backtrace. Con số trong ngoặc, ở đây là (12345), là ID của luồng. Mỗi lần chạy bạn sẽ thấy một số khác, và các phiên bản Rust cũ hơn không in nó ra.
Khi chạy, chương trình in ra thông báo đó rồi dừng lại: không có gì sau panic! được thực thi.
Những thứ gây panic dù bạn không gọi panic! trực tiếp
Phần tiêu đề “Những thứ gây panic dù bạn không gọi panic! trực tiếp”Vài thao tác bạn đã gặp sẽ tự panic nếu có gì đó sai. Mỗi ví dụ dưới đây đọc giá trị nguy hiểm qua tham số của hàm chứ không viết cứng một literal. Nếu chỉ số hay số 0 được viết thẳng trong code, trình biên dịch thường tính ra ngay lúc biên dịch rằng thao tác luôn thất bại, và từ chối build luôn thay vì để chương trình chạy rồi panic.
fn get(numbers: &[i32], index: usize) -> i32 { numbers[index] // panic: chỉ số vượt ngoài phạm vi}
fn main() { let numbers = [1, 2, 3]; println!("{}", get(&numbers, 10));}fn main() { let value: Option<i32> = None; println!("{}", value.unwrap()); // panic: called `Option::unwrap()` on a `None` value}fn divide(a: i32, b: i32) -> i32 { a / b // panic: attempt to divide by zero (chia cho 0)}
fn main() { let a = 10; let b = 0; println!("{}", divide(a, b));}Mỗi trường hợp trên đều là Rust từ chối để chương trình chạy tiếp trong một trạng thái vô nghĩa, thay vì lặng lẽ trả về dữ liệu rác.
panic! hay Result
Phần tiêu đề “panic! hay Result”Đây là quyết định thật sự mà bạn sẽ gặp thường xuyên: thất bại này nên làm sập chương trình, hay nơi gọi nên có cơ hội xử lý nó?
panic!: cho lỗi trong chương trình (bug) và những tình huống không bao giờ được xảy ra nếu code đúng. Không có cách hợp lý nào để phục hồi, nên dừng ngay (trước khi gây thêm hậu quả) là lựa chọn an toàn nhất.Result(có ở trang trước): cho những thất bại mà nơi gọi có thể lường trước và muốn xử lý, như người dùng nhập sai, thiếu file, hay một yêu cầu mạng thất bại.
// Người dùng nhập sai: có thể phục hồi, dùng Resultfn parse_age(input: &str) -> Result<u32, String> { input.parse().map_err(|_| String::from("tuổi không hợp lệ"))}
// Một bất biến không thể sai nếu code đúng: panicfn get_first(numbers: &[i32]) -> i32 { if numbers.is_empty() { panic!("get_first được gọi với một slice rỗng"); } numbers[0]}unwrap() và expect() là lối tắt dẫn đến panic
Phần tiêu đề “unwrap() và expect() là lối tắt dẫn đến panic”Bạn đã thấy .unwrap() trên cả Option lẫn Result: thật ra nó chỉ là lối tắt cho “đưa tôi giá trị, hoặc panic nếu không có”. .expect("thông báo") làm y như vậy nhưng cho bạn gắn kèm một thông báo hữu ích hơn:
fn main() { let input = "42"; let number: i32 = input.parse().expect("đầu vào phải là một số hợp lệ"); println!("{number}");}Nếu chuyển đổi thất bại, thông báo panic sẽ chứa đoạn chữ của bạn, giúp gỡ lỗi dễ hơn nhiều so với thông báo chung chung mà .unwrap() đưa ra.
Kiểm tra giả định: assert!, assert_eq! và unreachable!
Phần tiêu đề “Kiểm tra giả định: assert!, assert_eq! và unreachable!”Đôi khi bạn biết chắc một điều phải đúng tại một chỗ nào đó trong code. Rust có vài macro nhỏ biến niềm tin đó thành một phép kiểm tra. Nếu kiểm tra thất bại, chúng panic kèm thông báo rõ ràng, nhờ vậy lỗi lộ ra ngay tại nơi nó bắt đầu thay vì gây ra kết quả kỳ lạ về sau.
assert!(condition, "message")panic nếu điều kiện làfalse.assert_eq!(left, right)panic nếu hai giá trị không bằng nhau, và in ra cả hai.unreachable!("message")đánh dấu code không bao giờ được chạy, như một nhánhmatchcho giá trị không thể xảy ra.
fn average(numbers: &[i32]) -> i32 { assert!(!numbers.is_empty(), "average cần ít nhất một số");
let mut total = 0; for n in numbers { total += n; } total / numbers.len() as i32}
fn day_name(day: u8) -> &'static str { match day { 1 => "Thứ Hai", 2 => "Thứ Ba", 3 => "Thứ Tư", 4 => "Thứ Năm", 5 => "Thứ Sáu", 6 => "Thứ Bảy", 7 => "Chủ Nhật", _ => unreachable!("day phải từ 1 đến 7, nhận được {day}"), }}
fn main() { let avg = average(&[70, 80, 90]); assert_eq!(avg, 80); println!("trung bình: {avg}");
println!("ngày 3 là {}", day_name(3));}Kết quả:
trung bình: 80ngày 3 là Thứ TưỞ đây mọi kiểm tra đều qua, nên chương trình chạy bình thường. Giờ hãy đổi danh sách thành [70, 80, 95] trong Playground. Trung bình thành 81, và assert_eq! dừng chương trình, in hai giá trị cạnh nhau:
thread 'main' (12345) panicked at src/main.rs:26:5:assertion `left == right` failed left: 81 right: 80Rust báo bằng tiếng Anh rằng phép so sánh left == right thất bại: bên trái là 81, bên phải là 80. Hãy thử cả average(&[]) hoặc day_name(9), bạn sẽ thấy thông báo của chính mình trong panic.
Các kiểm tra này luôn chạy, kể cả trong bản release, nên hãy giữ chúng thật nhẹ. Bạn sẽ gặp lại assert_eq! ở trang Kiểm thử, nơi nó là cách chính để một bài test nói “cái này phải bằng cái kia”. Ngoài ra còn có todo!(), panic với thông báo “not yet implemented” (chưa được cài đặt). Nó tiện làm thân hàm tạm thời khi bạn phác thảo một hàm sẽ hoàn thiện sau.
Luyện tập
Phần tiêu đề “Luyện tập”1. Số cổng với expect
Phần tiêu đề “1. Số cổng với expect”Bạn có một giá trị cấu hình let port_text = "8080";. Hãy chuyển nó thành u16 bằng .parse() và .expect(...), với một thông báo giải thích điều gì đã sai. Rồi in ra Đang lắng nghe ở cổng 8080. Gợi ý: biến cần có kiểu, let port: u16 = ..., để parse biết phải tạo ra gì. Sau đó, đổi văn bản thành "80a0" để thấy thông báo của bạn trong panic.
Xem lời giải
fn main() { let port_text = "8080"; let port: u16 = port_text .parse() .expect("cổng trong cấu hình phải là một số từ 0 đến 65535"); println!("Đang lắng nghe ở cổng {port}");}Kết quả:
Đang lắng nghe ở cổng 80802. Chia tiền hoá đơn
Phần tiêu đề “2. Chia tiền hoá đơn”Hãy viết fn split_bill(total: u32, people: u32) -> u32 trả về phần tiền mỗi người phải trả. Chia cho 0 người là lỗi ở phía code gọi hàm, nên hãy dùng assert! với thông báo rõ ràng để bắt nó. In phần chia cho tổng 120 và 4 người. Gợi ý: đặt assert! ở dòng đầu tiên của hàm, trước phép chia.
Xem lời giải
fn split_bill(total: u32, people: u32) -> u32 { assert!(people > 0, "split_bill cần ít nhất một người"); total / people}
fn main() { let share = split_bill(120, 4); println!("Mỗi người trả {share}");}Kết quả:
Mỗi người trả 30Tóm tắt
Phần tiêu đề “Tóm tắt”panic!dừng chương trình ngay lập tức kèm một thông báo lỗi.- Một số thao tác chuẩn (chỉ số mảng sai,
.unwrap()trênNone, chia cho 0) tự động panic. - Dùng
panic!cho bug và những bất biến không thể sai; dùngResultcho thất bại mà nơi gọi có thể xử lý. .expect("thông báo")giống.unwrap(), nhưng có thêm thông báo giúp gỡ lỗi panic dễ hơn.assert!,assert_eq!vàunreachable!cố ý panic khi một điều bạn tin là đúng hoá ra lại sai.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Gọi panic! có tác dụng gì?
2. Theo quy tắc nhanh trên trang này, khi nào nên dùng panic! thay vì Result?
3. Khác biệt giữa .unwrap() và .expect('thông báo') là gì?
Điểm: 0 / 3
Đọc thêm
Phần tiêu đề “Đọc thêm”- Unrecoverable Errors with panic! — The Rust Book (tiếng Anh): chuyện gì xảy ra khi panic, quá trình unwinding, và cách đọc backtrace.
- To panic! or Not to panic! — The Rust Book (tiếng Anh): hướng dẫn chính thức để chọn giữa
panic!vàResult, kèm ví dụ. - panic! — Rust by Example (tiếng Anh): một ví dụ ngắn về panic, cho thấy bộ nhớ vẫn được giải phóng khi chương trình unwind.
assert_eq!— tài liệu std (tiếng Anh): các macro assert hoạt động thế nào, kể cả thông báo tuỳ chỉnh.panic!— tài liệu std (tiếng Anh): tài liệu tham khảo cho chínhpanic!, nó liên quan thế nào tớiunwrap, và khi nào panic là công cụ phù hợp.