Lỗi Rust E0004: non-exhaustive patterns not covered (match chưa xét hết trường hợp)
E0004 nghĩa là match của bạn bỏ sót ít nhất một giá trị có thể có, và Rust từ chối đoán xem nên làm gì trong trường hợp đó. Đây là một trong những lỗi thân thiện nhất của Rust: trình biên dịch nói chính xác trường hợp nào bị thiếu, thậm chí còn viết sẵn dòng code cho bạn. Trang này cho bạn xem cách sửa trước, rồi giải thích vì sao phép kiểm tra này là một trong những tính năng hay nhất của Rust. Các lỗi khác trong loạt bài này nằm ở lỗi trình biên dịch Rust.
Thông báo lỗi
Phần tiêu đề “Thông báo lỗi”error[E0004]: non-exhaustive patterns: `OrderStatus::Delivered` not covered --> src/main.rs:8:11 | 8 | match status { | ^^^^^^ pattern `OrderStatus::Delivered` not covered |note: `OrderStatus` defined here --> src/main.rs:1:6 | 1 | enum OrderStatus { | ^^^^^^^^^^^... 4 | Delivered, // added later | --------- not covered = note: the matched value is of type `OrderStatus`help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown |10 ~ OrderStatus::Shipped => "on its way",11 ~ OrderStatus::Delivered => todo!(), |Trình biên dịch báo rằng mẫu OrderStatus::Delivered chưa được xử lý, và gợi ý thêm một nhánh cho nó.
Đây là chương trình gây ra lỗi:
enum OrderStatus { Pending, Shipped, Delivered, // added later}
fn describe(status: OrderStatus) -> &'static str { match status { OrderStatus::Pending => "waiting to be packed", OrderStatus::Shipped => "on its way", // ❌ nothing for Delivered }}
fn main() { println!("{}", describe(OrderStatus::Delivered));}Ví dụ lỗi này giữ nguyên tiếng Anh vì các dòng của nó xuất hiện trong thông báo lỗi. Delivered được thêm vào sau, và match chưa có nhánh nào cho nó.
Phần chữ giữa hai dấu backtick thay đổi theo code của bạn. Bạn có thể thấy `None` not covered, `&_` not covered, `0_u8` and `4_u8..=u8::MAX` not covered hoặc `(true, false)` and `(false, true)` not covered. Lần nào cũng là cùng một lỗi: Rust đang liệt kê những giá trị mà match của bạn không có nhánh nào cho chúng.
Ý nghĩa
Phần tiêu đề “Ý nghĩa”Đọc thông báo từ trên xuống dưới:
- Dòng 8: dấu
^^^^^^chỉ vào giá trị bạn đang so khớp,status. - “pattern
OrderStatus::Deliverednot covered”: đây là trường hợp bạn quên. - Dòng note nhảy tới định nghĩa enum và đánh dấu biến thể (variant) bị quên ở dòng 4.
- Dòng help cho thấy chính xác nhánh cần thêm.
todo!()là chỗ giữ chỗ; bạn thay nó bằng code thật.
Hãy nghĩ tới một máy bán hàng tự động có ba nút, nhưng chỉ hai nút được nối dây. Bấm nút thứ ba thì sẽ… làm gì? Rust không cho bạn xuất xưởng một cái máy có nút chết. Nút nào cũng cần một câu trả lời, dù câu trả lời chỉ là “tạm hết hàng”.
Vì sao Rust làm vậy
Phần tiêu đề “Vì sao Rust làm vậy”Một match trong Rust phải xét hết mọi trường hợp (exhaustive): với mỗi giá trị mà kiểu có thể chứa, phải có đúng một nhánh áp dụng. Nếu thiếu một nhánh, chương trình sẽ tới một điểm không có code nào để chạy. Các ngôn ngữ khác thì lặng lẽ đi tiếp, trả về một giá trị mặc định, hoặc crash lúc chạy. Rust bắt được nó trước khi chương trình kịp chạy. Bài match trình bày phần cơ bản.
Lợi ích thật sự đến về sau, khi code của bạn lớn dần. Giả sử bạn thêm một biến thể mới vào một enum đang được match ở mười chỗ trong project:
enum OrderStatus { Pending, Shipped, Delivered, Returned, // mới!}Lúc này trình biên dịch đưa ra một lỗi E0004 cho mỗi match cần cập nhật:
error[E0004]: non-exhaustive patterns: `&OrderStatus::Returned` not covered --> src/main.rs:9:11error[E0004]: non-exhaustive patterns: `&OrderStatus::Returned` not covered --> src/main.rs:17:11Đó là một danh sách việc cần làm được viết sẵn cho bạn. Bạn không phải lục khắp code và cầu mong mình đã tìm đủ mọi chỗ. Đây là lý do những lập trình viên Rust có kinh nghiệm rất thích enum: bạn có thể thay đổi chúng mà không sợ gì.
Cách sửa
Phần tiêu đề “Cách sửa”Cách sửa 1: Thêm nhánh còn thiếu
Phần tiêu đề “Cách sửa 1: Thêm nhánh còn thiếu”Chọn cách này cho các enum của chính bạn. Đây là cách sửa đúng trong phần lớn trường hợp, và đúng là điều dòng help: của rustc gợi ý. Thêm nhánh, rồi thay todo!() bằng điều thật sự cần xảy ra:
enum OrderStatus { Pending, Shipped, Delivered,}
fn describe(status: OrderStatus) -> &'static str { match status { OrderStatus::Pending => "đang chờ đóng gói", OrderStatus::Shipped => "đang trên đường giao", OrderStatus::Delivered => "đã tới cửa nhà bạn", // ✅ mọi trường hợp đều được xử lý }}
fn main() { println!("{}", describe(OrderStatus::Pending)); println!("{}", describe(OrderStatus::Delivered));}Kết quả:
đang chờ đóng góiđã tới cửa nhà bạnNếu nhiều biến thể cần làm cùng một việc, hãy nối chúng bằng | trong một nhánh: OrderStatus::Shipped | OrderStatus::Delivered => false. Bạn vẫn nêu tên từng biến thể, nên trình biên dịch vẫn cảnh báo được khi có biến thể mới. Option cũng vậy: nếu thông báo lỗi ghi `None` not covered, hãy thêm một nhánh None => .... Xem Option.
Cách sửa 2: Dùng khoảng và _ cho số và văn bản
Phần tiêu đề “Cách sửa 2: Dùng khoảng và _ cho số và văn bản”Chọn cách này khi bạn so khớp trên số nguyên, char, hoặc chuỗi. Một u8 có 256 giá trị có thể, còn &str thì vô số, nên bạn không thể liệt kê hết. Hãy dùng khoảng như 1..=9 cho các nhóm số, và kết thúc bằng _ (hoặc một cái tên như other) cho “mọi thứ còn lại”:
fn grade(score: u8) -> char { match score { 90..=100 => 'A', 75..=89 => 'B', 50..=74 => 'C', 0..=49 => 'F', _ => '?', // 101 tới 255: không phải điểm hợp lệ }}
fn run(command: &str) { match command { "start" => println!("Bắt đầu hẹn giờ"), "stop" => println!("Dừng hẹn giờ"), other => println!("Lệnh không rõ: {other}"), }}
fn main() { println!("{} {} {}", grade(95), grade(62), grade(150)); run("start"); run("pause");}Kết quả:
A C ?Bắt đầu hẹn giờLệnh không rõ: pauseVới chuỗi, rustc còn nói thẳng: `&str` cannot be matched exhaustively, so a wildcard `_` is necessary (&str không thể xét hết mọi trường hợp, nên cần ký tự đại diện _). Với số, Rust kiểm tra các khoảng của bạn rất chính xác. Nếu chúng thật sự phủ mọi giá trị (ví dụ từ 0..=49 lên tới 90..=u8::MAX), bạn hoàn toàn không cần _. Đọc thêm ở So khớp một khoảng.
Cách sửa 3: Dùng if let hoặc let else khi bạn chỉ quan tâm một trường hợp
Phần tiêu đề “Cách sửa 3: Dùng if let hoặc let else khi bạn chỉ quan tâm một trường hợp”Chọn cách này khi bạn chỉ muốn xử lý một mẫu và bỏ qua phần còn lại. Một match đầy đủ với một nhánh rỗng “không làm gì” vẫn chạy được, nhưng if let nói điều đó rõ hơn. let else dành cho tình huống “lấy giá trị ra, hoặc rời khỏi hàm sớm”:
fn greet(nickname: Option<&str>) { if let Some(name) = nickname { println!("Chào {name}!"); } // không có biệt danh: không làm gì}
fn first_word_len(text: &str) -> usize { let Some(word) = text.split_whitespace().next() else { return 0; // văn bản rỗng: rời hàm sớm }; word.len()}
fn main() { greet(Some("Ferris")); greet(None); println!("{}", first_word_len("hello world")); println!("{}", first_word_len(" "));}Kết quả:
Chào Ferris!50Chuỗi "hello world" giữ tiếng Anh vì len() đếm theo byte, và chữ tiếng Việt có dấu chiếm nhiều byte hơn. Khối else của let else phải rời khỏi khối hiện tại (return, break, continue hoặc panic). Xem if let và let-else.
Cách sửa 4: Nhánh _ => bắt mọi thứ còn lại (có đánh đổi)
Phần tiêu đề “Cách sửa 4: Nhánh _ => bắt mọi thứ còn lại (có đánh đổi)”Chọn cách này khi phần lớn các biến thể của một enum thật sự làm cùng một việc. _ khớp với mọi thứ mà các nhánh phía trên chưa khớp:
enum Key { Up, Down, Left, Right, Enter, Escape,}
fn handle(key: Key) { match key { Key::Enter => println!("Xác nhận"), Key::Escape => println!("Hủy"), _ => println!("Di chuyển con trỏ"), }}
fn main() { handle(Key::Enter); handle(Key::Left); handle(Key::Escape);}Kết quả:
Xác nhậnDi chuyển con trỏHủyHãy thẳng thắn về cái giá: _ tắt luôn phép kiểm tra ở mục Vì sao Rust làm vậy. Nếu sau này bạn thêm Key::Delete, trình biên dịch sẽ im lặng, và bấm Delete sẽ di chuyển con trỏ. Đó là một lỗi mà Rust lẽ ra đã bắt giúp bạn. Với enum của chính bạn, hãy ưu tiên liệt kê các biến thể bằng | (Key::Up | Key::Down | Key::Left | Key::Right => ...). Để dành _ cho số, chuỗi, và enum từ crate khác.
Thường gặp ở đâu
Phần tiêu đề “Thường gặp ở đâu”Option không có None. match nickname { Some(name) => ... } cho ra `None` not covered. Cách sửa: thêm một nhánh None =>, hoặc dùng if let Some(name) = nickname nếu không có gì cần làm với None.
Tuple và kiểu boolean. So khớp hai bool cùng lúc có bốn tổ hợp, và rất dễ quên vài cái:
match (logged_in, is_admin) { (true, true) => println!("Welcome, admin"), (false, false) => println!("Please log in"),} // ❌ patterns `(true, false)` and `(false, true)` not coveredCách sửa: thêm các nhánh còn thiếu, hoặc dùng _ bên trong tuple, như (false, _) => println!("Please log in"), nhánh này phủ cả hai trường hợp “chưa đăng nhập”. Xem So khớp nhiều giá trị cùng lúc.
Một mẫu trong let. Viết let Some(score) = saved; là một let có thể không khớp. Đây là một lỗi “chị em”, E0005, với cùng ý tưởng:
error[E0005]: refutable pattern in local binding --> src/main.rs:3:9 |3 | let Some(score) = saved; | ^^^^^^^^^^^ pattern `None` not covered | = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one varianthelp: you might want to use `let...else` to handle the variant that isn't matched |3 | let Some(score) = saved else { todo!() }; | ++++++++++++++++Rust báo rằng mẫu này có thể không khớp (trường hợp None), trong khi let cần một mẫu luôn khớp, và gợi ý dùng let...else. Cách sửa: let Some(score) = saved else { return; };, hoặc if let, như ở Cách sửa 3. Xem Mẫu trong let.
Enum từ crate khác. Một số enum, như std::io::ErrorKind, được đánh dấu #[non_exhaustive]. Tác giả của chúng có thể thêm biến thể về sau, nên dù bạn liệt kê mọi biến thể đang thấy hôm nay, rustc vẫn nói `_` not covered. Cách sửa: thêm một nhánh _ =>. Ở đây nó là bắt buộc, không phải đường tắt.
Lỗi liên quan
Phần tiêu đề “Lỗi liên quan”- E0005 (refutable pattern in local binding): cùng phép kiểm tra “not covered”, nhưng cho
letthay vìmatch. Đã trình bày ở trên. - E0308: mismatched types, ví dụ khi các nhánh
matchtrả về các kiểu khác nhau ("text"ở một nhánh, một con số ở nhánh khác). - E0382: borrow of partially moved value, khi một nhánh như
Some(name)move mộtStringra ngoài và bạn lại dùng bản gốc sau đó. Hãy so khớp trên&nickname. - E0507: cannot move out of a borrowed value, khi một nhánh
matchcố lấy mộtStringra khỏi thứ bạn chỉ đang mượn. - Tất cả lỗi trình biên dịch: danh sách đầy đủ các trang lỗi.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Bạn thêm biến thể Refunded vào một enum. Trình biên dịch làm gì với các biểu thức match hiện có đang liệt kê từng biến thể theo tên?
2. Bạn so khớp một &str với "yes" và "no". Sửa E0004 thế nào?
3. Nhược điểm của _ => trong một match trên enum của chính bạn là gì?
Điểm: 0 / 3
Đọc thêm
Phần tiêu đề “Đọc thêm”- E0004 — Rust error codes index (tiếng Anh): giải thích chính thức của lỗi này.
- E0005 — Rust error codes index (tiếng Anh): lỗi “chị em” dành cho mẫu trong
let. - The match Control Flow Construct — The Rust Book (tiếng Anh): chương giải thích vì sao
matchphải xét hết mọi trường hợp, và ký tự giữ chỗ_. - Refutability — The Rust Book (tiếng Anh): khi nào một mẫu có thể không khớp, và nó được phép ở đâu.
- match — Rust by Example (tiếng Anh): các ví dụ ngắn chạy được về khoảng,
|và_trong các nhánhmatch.