跳转到内容

Rust 错误 E0005:refutable pattern in local binding(let 中的可反驳模式)

E0005 的意思是:你写的 let 用了一个可能匹配不上的模式,比如 value 有可能是 None,你却写了 let Some(x) = value;。Rust 需要知道匹配不上时该怎么办,而普通的 let 没有地方让你写这个。修法就是给“没匹配上”的情况找个去处:通常是 let ... else、if let 或 match。刚开始用 Option 和 Result 时,这个错误非常常见,修起来一分钟就够了。这个系列的其他错误,请看 Rust 编译错误详解。

error[E0005]: refutable pattern in local binding
--> src/main.rs:4:9
|
4 | let Some(top) = best; // ❌ what if the list was empty?
| ^^^^^^^^^ pattern `None` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
= note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
= note: the matched value is of type `Option<i32>`
help: you might want to use `let...else` to handle the variant that isn't matched
|
4 | let Some(top) = best else { todo!() }; // ❌ what if the list was empty?
| ++++++++++++++++

第一行的意思是“局部绑定中出现了可反驳的模式”。第一条 note 说:let 绑定需要“不可反驳的模式”,比如结构体,或者只有一个变体的枚举。help 建议你“用 let...else 处理没匹配上的那个变体”。

产生这个错误的程序是这样的:

fn main() {
let scores = vec![72, 95, 88];
let best = scores.iter().max().copied();
let Some(top) = best; // ❌ what if the list was empty?
println!("Top score: {top}");
}

换成别的模式,你也会看到同样的错误,变的只有“not covered”(没有覆盖到)那一部分:

3 | let Ok(age) = input.parse::<i32>();
| ^^^^^^^ pattern `Err(_)` not covered
3 | let [host, port] = parts.as_slice();
| ^^^^^^^^^^^^ patterns `&[]`, `&[_]` and `&[_, _, _, ..]` not covered

关键在两个词:

  • 可反驳(refutable)的模式可能匹配失败。Some(top) 在值为 None 时失败,Ok(age) 遇到 Err 时失败,[host, port] 在 slice 不是正好两个元素时失败。
  • 不可反驳(irrefutable)的模式总能匹配。像 let top = best; 这样只写一个名字总是行得通的,元组和结构体也一样,比如 let (x, y) = point;。

let 必须总是成功,因为下一行要用到它创建的变量。如果 best 是 None,下一行的 top 该是什么?没有答案,所以 Rust 拦住了你。“not covered”标注准确列出了你漏掉的情况:None、Err(_),或者对 slice 来说,“空的”“只有一个元素”和“三个或更多元素”。

可以想象一个快递柜。let Some(top) = best; 就像你连柜门都没打开,就笃定里面有包裹,转身走了。Rust 要求你先想好:柜子是空的话怎么办。

let、函数参数和 for 循环都需要不会失败的模式。match、if let、while let 和 let ... else 可以接受会失败的模式,因为它们都有地方处理另一种情况。这和 E0004 是同一条规则:match 必须覆盖所有的值。在这里,“match”就是一个只有一个分支的 let。

在很多语言里,读取一个不存在的值,会得到 null、undefined,或者在运行时崩溃。而在 Rust 里,空列表、文本框里填错的数字、缺少的设置项,都是要在代码里处理的正常情况。Option 和 Result 让它们一目了然,E0005 则确保你不会不小心跳过它们。参见在 let 和函数参数里使用模式和 Option。

当函数剩下的部分需要这个值、没有它就没什么可做时,选这个。这也是 rustc 的 help: 给出的建议。模式保持不变,再加一个 else 块。这个块必须离开当前代码:return、break、continue 或者 panic。

fn port_from(text: &str) -> u16 {
// 把数字取出来,取不出来就提前离开函数
let Ok(port) = text.trim().parse::<u16>() else {
println!("“{text}”不是有效的端口,改用 8080");
return 8080;
};
port
}
fn main() {
let scores = vec![72, 95, 88];
let Some(top) = scores.iter().max() else {
println!("还没有分数");
return;
};
println!("最高分:{top}");
println!("端口:{}", port_from("3000"));
println!("端口:{}", port_from("abc"));
}

输出:

最高分:95
端口:3000
“abc”不是有效的端口,改用 8080
端口:8080
▶ 动手试一试

别把 rustc 给的 else { todo!() } 原样留着。todo!() 一运行就会 panic,要把它换成真正的退出方式。详见 let-else。

修复 2:两种情况都要做事时,用 match 或 if let

Section titled “修复 2:两种情况都要做事时,用 match 或 if let”

当“没匹配上”的情况有自己的事要做(比如打印错误),或者无论哪种情况代码都要继续往下走时,选这个。match 让你处理每一种情况,还能用上错误值。不知道 slice 有多长时,也要用它来拆开 slice。只关心其中一种情况时,就用 if let:

fn main() {
// match:两种结果都处理,还能用上错误
for input in ["42", "forty-two"] {
match input.parse::<i32>() {
Ok(age) => println!("年龄:{age}"),
Err(e) => println!("无法读取“{input}”:{e}"),
}
}
// slice 可以是任意长度:按它的形状来匹配
for address in ["localhost:8080", "localhost"] {
let parts: Vec<&str> = address.split(':').collect();
match parts.as_slice() {
[host, port] => println!("host = {host}, port = {port}"),
_ => println!("“{address}”应该写成 host:port 的形式"),
}
}
// if let:只在有值的时候做事
let nickname: Option<&str> = None;
if let Some(name) = nickname {
println!("你好,{name}!");
} else {
println!("你好,陌生人!");
}
}

输出:

年龄:42
无法读取“forty-two”:invalid digit found in string
host = localhost, port = 8080
“localhost”应该写成 host:port 的形式
你好,陌生人!
▶ 动手试一试

invalid digit found in string(字符串里有无效的数字)是标准库自己的错误信息,所以保持英文。_ 分支兜住了其他所有长度。没有它,match 会给你 E0004,附上同样的“not covered”列表。参见 if let 和 match。

修复 3:用 unwrap_or 给默认值,或者用 ? 交给调用方

Section titled “修复 3:用 unwrap_or 给默认值,或者用 ? 交给调用方”

当有一个合理的默认值,或者应该让调用方去处理问题时,选这个。unwrap_or(x) 有值就给你这个值,没有就给 x。? 用在返回 Result(或 Option)的函数里:它把错误交还给调用方,并结束当前函数。

use std::num::ParseIntError;
// ? 把错误交给调用方,而不是在这里处理
fn total_minutes(hours: &str, minutes: &str) -> Result<i32, ParseIntError> {
let h = hours.parse::<i32>()?;
let m = minutes.parse::<i32>()?;
Ok(h * 60 + m)
}
fn main() {
// 什么都没有时,用默认值
let scores: Vec<i32> = Vec::new();
let top = scores.iter().max().copied().unwrap_or(0);
println!("最高分:{top}");
let volume = "loud".parse::<u8>().unwrap_or(50);
println!("音量:{volume}");
println!("{:?}", total_minutes("2", "30"));
println!("{:?}", total_minutes("2", "half"));
}

输出:

最高分:0
音量:50
Ok(150)
Err(ParseIntError { kind: InvalidDigit })
▶ 动手试一试

如果你确定值一定存在(比如是你刚在代码里写下的数字),用 .expect("原因") 也可以。万一你判断错了,它会带着你的信息 panic,所以只把它留给真的不会失败的情况。在什么都不返回的 main 里用 ?,会得到 E0277;参见 ? 运算符和快捷写法:unwrap_or 和 unwrap。

修复 4:换一个模式不会失败的类型

Section titled “修复 4:换一个模式不会失败的类型”

当数据的形状总是一样、模式失败只是因为你选的类型不合适时,选这个。slice 或 Vec 可以是任意长度,但 [u8; 3] 这样的数组总是正好有三个元素,所以 let [r, g, b] = color; 总能匹配。元组和结构体也总能匹配。还有 split_once,它把前后两半放在一个元组里返回,这样你只需要处理一个 Option,而不是一个长度未知的 slice:

struct Point {
x: i32,
y: i32,
}
fn main() {
// 数组长度固定:这个模式总能匹配
let color: [u8; 3] = [255, 140, 0];
let [r, g, b] = color;
println!("r={r} g={g} b={b}");
// 元组和结构体也总能匹配
let (host, port) = ("localhost", 8080);
let Point { x, y } = Point { x: 3, y: -1 };
println!("{host}:{port} 位于 ({x}, {y})");
// split_once 返回一个装着前后两半的 Option
let Some((key, value)) = "theme=dark".split_once('=') else {
return;
};
println!("{key} -> {value}");
}

输出:

r=255 g=140 b=0
localhost:8080 位于 (3, -1)
theme -> dark
▶ 动手试一试

你自己定义的枚举也一样:let Shape::Circle(radius) = shape; 会失败,并提示 pattern `Shape::Square(_)` not covered。如果这个值真的只可能是圆,那它也许应该是一个 Circle 结构体,而不是枚举。参见数组和结构体。

用 for 循环遍历一组 Option。 for Some(temp) in readings 会得到“refutable pattern in for loop binding”(for 循环绑定中的可反驳模式)。修复:for temp in readings.iter().flatten() 会跳过所有 None;或者遍历每个元素,在循环里用 if let Some(t) = reading。

函数参数。 fn greet(Some(name): Option<&str>) 会得到“refutable pattern in function argument”(函数参数中的可反驳模式)。修复:接收 name: Option<&str>,在函数里 match;或者接收 name: &str,让调用方决定遇到 None 怎么办。

解析输入。 读取用户输入或命令行参数时写了 let Ok(n) = line.trim().parse::<i32>();。修复:在输入循环里写 let Ok(n) = ... else { println!("请输入一个数字"); continue; };,或者用 match 把错误打印出来。

小测验

1. 下面哪个 let 会得到 E0005?

2. let ... else 的 else 块必须做什么?

3. 你要从文本里取出一个数字,如果文本不是数字,用 0 作为默认值就行。最短的修法是什么?

得分: 0 / 3