跳转到内容

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 没有分支处理的那些值。

从上往下读这条信息:

  1. 第 8 行:^^^^^^ 指向你要匹配的值 status。
  2. “pattern OrderStatus::Delivered not covered”:这就是你漏掉的情况。
  3. note 跳到枚举的定义处,在第 4 行标出了被遗忘的变体。
  4. help 直接给出要补的分支。todo!() 只是个占位符,你要把它换成真正的代码。

想象一台有三个按钮的自动售货机,却只接好了两个按钮的线。按下第三个按钮会发生什么?没人知道。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:11
error[E0004]: non-exhaustive patterns: `&OrderStatus::Returned` not covered
--> src/main.rs:17:11

这就是一张替你写好的待办清单。你不用在整个代码库里搜索,还担心有没有漏掉哪里。这也是有经验的 Rust 开发者这么喜欢枚举的原因:改起来一点都不用怕。

自己定义的枚举,就选这个方法。大多数时候这都是正确的修法,也正是 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。

匹配整数、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!
5
0
▶ 动手试一试

"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 variant
help: 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