Rust 错误 E0004:non-exhaustive patterns not covered(match 没有穷尽所有情况)
E0004 的意思是:你的 match 漏掉了至少一种可能的值,而 Rust 不肯替你猜这种情况该怎么办。它是 Rust 里最友好的错误之一:编译器会准确告诉你漏了哪种情况,甚至把要补的那一行都替你写好。这一页先讲怎么修,再解释为什么这项检查是 Rust 最好用的特性之一。这个系列的其他错误,请看 Rust 编译错误详解。
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, // 后来才加上的 | --------- 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 => "运输中",11 ~ OrderStatus::Delivered => todo!(), |“non-exhaustive patterns: OrderStatus::Delivered not covered” 的意思是“模式没有穷尽:没有覆盖 OrderStatus::Delivered”。help 建议你加一个通配模式的分支,或者像下面这样写出具体的模式,确保所有情况都处理到。
产生这个错误的程序是这样的:
enum OrderStatus { Pending, Shipped, Delivered, // 后来才加上的}
fn describe(status: OrderStatus) -> &'static str { match status { OrderStatus::Pending => "等待打包", OrderStatus::Shipped => "运输中", // ❌ 没有处理 Delivered }}
fn main() { println!("{}", describe(OrderStatus::Delivered));}反引号里的内容会随你的代码变化。你可能会看到 `None` not covered、`&_` not covered、`0_u8` and `4_u8..=u8::MAX` not covered 或 `(true, false)` and `(false, true)` not covered。每次都是同一个错误:Rust 在列出你的 match 没有分支处理的那些值。
这个错误是什么意思
Section titled “这个错误是什么意思”从上往下读这条信息:
- 第 8 行:
^^^^^^指向你要匹配的值status。 - “pattern
OrderStatus::Deliverednot covered”:这就是你漏掉的情况。 note跳到枚举的定义处,在第 4 行标出了被遗忘的变体。help直接给出要补的分支。todo!()只是个占位符,你要把它换成真正的代码。
想象一台有三个按钮的自动售货机,却只接好了两个按钮的线。按下第三个按钮会发生什么?没人知道。Rust 不允许你把带着“死按钮”的机器交出去。每个按钮都得有个回应,哪怕回应是“暂停服务”。
Rust 为什么要这样做
Section titled “Rust 为什么要这样做”Rust 的 match 必须是穷尽的(exhaustive):对于这个类型可能取的每一个值,都必须恰好有一个分支能处理它。如果少了一个分支,程序就会走到一个没有代码可执行的地方。别的语言会悄悄往下走、返回一个默认值,或者在运行时崩溃。Rust 在程序运行之前就把问题抓出来了。基础知识见 match 这一课。
真正的好处在之后,等你的代码变多的时候才会显现。假设项目里有 10 个地方对一个枚举做了 match,现在你给它加了一个新变体:
enum OrderStatus { Pending, Shipped, Delivered, Returned, // 新加的!}编译器会给每一个需要更新的 match 报一个 E0004:
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这就是一张替你写好的待办清单。你不用在整个代码库里搜索,还担心有没有漏掉哪里。这也是有经验的 Rust 开发者这么喜欢枚举的原因:改起来一点都不用怕。
修复 1:补上漏掉的分支
Section titled “修复 1:补上漏掉的分支”自己定义的枚举,就选这个方法。大多数时候这都是正确的修法,也正是 rustc 的 help: 给出的建议。把分支加上,再把 todo!() 换成真正该做的事:
enum OrderStatus { Pending, Shipped, Delivered,}
fn describe(status: OrderStatus) -> &'static str { match status { OrderStatus::Pending => "等待打包", OrderStatus::Shipped => "运输中", OrderStatus::Delivered => "已送达", // ✅ 所有情况都处理了 }}
fn main() { println!("{}", describe(OrderStatus::Pending)); println!("{}", describe(OrderStatus::Delivered));}输出:
等待打包已送达如果几个变体要做同样的事,可以用 | 把它们合并到一个分支里:OrderStatus::Shipped | OrderStatus::Delivered => false。每个变体仍然写出了名字,所以以后再加新变体时,编译器照样会提醒你。Option 也一样:如果错误说 `None` not covered,就补一个 None => ... 分支。参见 Option。
修复 2:数字和文本用范围和 _
Section titled “修复 2:数字和文本用范围和 _”匹配整数、char 或字符串时,选这个方法。u8 有 256 个可能的值,&str 更是有无穷多个,不可能一一列出。用 1..=9 这样的范围表示一组数字,最后再用 _(或者 other 这样的名字)表示“其他所有情况”:
fn grade(score: u8) -> char { match score { 90..=100 => 'A', 75..=89 => 'B', 50..=74 => 'C', 0..=49 => 'F', _ => '?', // 101 到 255:不是有效的分数 }}
fn run(command: &str) { match command { "start" => println!("计时开始"), "stop" => println!("计时停止"), other => println!("未知命令:{other}"), }}
fn main() { println!("{} {} {}", grade(95), grade(62), grade(150)); run("start"); run("pause");}输出:
A C ?计时开始未知命令:pause对于字符串,rustc 甚至会直接告诉你:`&str` cannot be matched exhaustively, so a wildcard `_` is necessary(&str 无法穷尽匹配,所以必须有一个通配符 _)。对于数字,Rust 会精确地检查你的范围。如果它们真的覆盖了所有值(比如从 0..=49 一直到 90..=u8::MAX),就根本不需要 _。详见匹配一个范围。
修复 3:只关心一种情况时,用 if let 或 let else
Section titled “修复 3:只关心一种情况时,用 if let 或 let else”只想对一种模式做处理、其他情况都忽略时,选这个方法。写一个完整的 match,再加一个“什么都不做”的空分支,当然也行,但 if let 表达得更清楚。let else 则用于“把值取出来,取不到就提前离开函数”:
fn greet(nickname: Option<&str>) { if let Some(name) = nickname { println!("你好,{name}!"); } // 没有昵称:什么都不做}
fn first_word_len(text: &str) -> usize { let Some(word) = text.split_whitespace().next() else { return 0; // 空文本:提前返回 }; word.len()}
fn main() { greet(Some("Ferris")); greet(None); println!("{}", first_word_len("hello world")); println!("{}", first_word_len(" "));}输出:
你好,Ferris!50"hello world" 保留英文,是因为 len() 返回的是字节数,换成中文结果就变了。let else 的 else 块必须离开当前代码块(return、break、continue 或者 panic)。参见 if let 和 let-else。
修复 4:用 _ => 兜底处理剩下的情况(有代价)
Section titled “修复 4:用 _ => 兜底处理剩下的情况(有代价)”当一个枚举的大多数变体确实要做同样的事时,选这个方法。_ 会匹配上面那些分支没匹配到的任何值:
enum Key { Up, Down, Left, Right, Enter, Escape,}
fn handle(key: Key) { match key { Key::Enter => println!("确认"), Key::Escape => println!("取消"), _ => println!("移动光标"), }}
fn main() { handle(Key::Enter); handle(Key::Left); handle(Key::Escape);}输出:
确认移动光标取消代价要说清楚:_ 会关掉Rust 为什么要这样做里讲的那项检查。如果以后你加了 Key::Delete,编译器不会吭声,按下 Delete 键就会移动光标。这本来是 Rust 能替你抓住的 bug。对于自己的枚举,最好用 | 把变体都列出来(Key::Up | Key::Down | Key::Left | Key::Right => ...)。_ 留给数字、字符串和其他 crate 里的枚举。
Option 没处理 None。 match nickname { Some(name) => ... } 会报 `None` not covered。修法:补一个 None => 分支;如果 None 时什么都不用做,就改用 if let Some(name) = nickname。
元组和布尔值。 同时匹配两个 bool 会有四种组合,很容易漏掉几种:
match (logged_in, is_admin) { (true, true) => println!("欢迎你,管理员"), (false, false) => println!("请先登录"),} // ❌ patterns `(true, false)` and `(false, true)` not covered修法:补上漏掉的分支,或者在元组里用 _,比如 (false, _) => println!("请先登录"),这样“未登录”的两种情况就都覆盖了。参见一次匹配多个值。
let 里的模式。 写 let Some(score) = saved; 这样的 let,可能会匹配失败。这是它的“姐妹错误” E0005,道理完全一样:
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!() }; | ++++++++++++++++这里说的是:let 绑定需要一个“不可反驳的模式”,而 Some(score) 在值为 None 时会失败。修法:写成 let Some(score) = saved else { return; };,或者像修复 3 那样用 if let。参见在 let 中使用模式。
其他 crate 里的枚举。 有些枚举(比如 std::io::ErrorKind)标了 #[non_exhaustive]。它们的作者以后可能还会加变体,所以就算你把今天能看到的变体全列出来,rustc 还是会说 `_` not covered。修法:加一个 _ => 分支。在这里它是必须的,不是偷懒。
- E0005(refutable pattern in local binding):同样的“not covered”检查,只不过用在
let上而不是match上。上面已经展示过。 - E0308:类型不匹配,比如 match 的各个分支返回了不同的类型(一个分支是
"text",另一个是数字)。 - E0382:borrow of partially moved value(使用了部分移动的值)。当
Some(name)这样的分支把一个String移了出去,之后你又用了原来的值,就会出现。改成对&nickname做匹配即可。 - E0507:cannot move out of a borrowed value(不能从借用的值中移出),当某个分支想从你只是借来的东西里拿走一个
String时出现。 - 所有编译错误:全部错误页面的列表。
小测验
1. 你给一个枚举加了变体 Refunded。对于那些按名字列出了每个变体的现有 match 表达式,编译器会怎么做?
2. 你用 match 把一个 &str 和 "yes"、"no" 比较。怎么修复 E0004?
3. 在自己的枚举上做 match 时,用 _ => 有什么缺点?
得分: 0 / 3
- E0004 — Rust 错误代码索引(英文):这个错误的官方说明。
- E0005 — Rust 错误代码索引(英文):
let中模式的“姐妹错误”。 - match 控制流结构 — Rust 程序设计语言:解释 match 为什么必须穷尽,以及
_占位符的章节。 - Refutability(可反驳性):模式是否会匹配失效 — Rust 程序设计语言:模式什么时候可能匹配失败,以及在哪里允许这样。
- match 匹配 — 通过例子学 Rust:match 分支中范围、
|和_的简短可运行示例。