模式匹配 用一个模式检查值的形状与内容,还能在同一步中取出内部数据;match 会把这套机制用于穷尽的分支选择。
_ 会让代码在枚举新增变体后继续编译,却可能悄悄走错分支;按值解构还可能移动非 Copy 字段,使原值整体失效。
对封闭枚举优先列出所有变体,先决定要借用还是取得所有权,再选择 match、if let、let-else 或 matches!。
是什么,为什么存在
Rust 的模式描述一个值必须具有的结构,并可为结构中的数据创建绑定。模式能匹配枚举变体、结构体字段、元组、切片、字面值和范围;它不是只比较一个标量的 switch 标签。
match 把一个待匹配值与若干分支从上到下比较,只执行第一个成功分支。编译器要求所有可能值都有去处,并警告永远到不了的分支,因此给枚举添加变体时,显式列举变体的调用点会立刻暴露遗漏。
模式还把检查和取值合成一次操作。Some(order) 同时证明 Option 不是 None,并把内部值绑定为 order;不需要先查询状态,再执行可能失效的取值。
你会在 match 分支、let、函数参数、for、if let、while let、let-else 和 matches! 中遇到模式。它们使用同一套模式语言,但允许失败的程度不同,所以不能只按代码长短互换。宏生成模式、复杂嵌套模式与 API 演进策略等内容属于 rust/pattern-matching-advanced,这里不重复展开。
工作原理
分支选择与穷尽性
match subject { pattern => expression, ... } 先计算一次 subject,再按源码顺序尝试分支。选中的分支产生整个 match 表达式的值,因此所有可到达分支必须得到兼容的结果类型。
穷尽性检查根据类型与模式覆盖范围进行,不会执行代码来猜测条件。布尔值只有两个可能值,普通枚举有已声明的变体,而整数通常需要范围或兜底模式;匹配守卫中的任意布尔表达式不算静态覆盖。
顺序会改变行为。较宽的范围、变量绑定或 _ 放在前面会遮住后面的精确模式,编译器通常以 unreachable_patterns 警告指出这种分支。
解构与绑定
模式既能测试,也能绑定。Request::Read { id } 先要求值是 Read 变体,再把字段绑定为局部变量 id;Request::Read { id: 0 } 则测试字段,但不创建同名变量。
_ 匹配并忽略一个值,.. 忽略结构或序列中剩余的部分。name @ subpattern 在要求 subpattern 成功的同时,把匹配到的完整部分绑定为 name,适合既要验证范围又要继续使用原值的场景。
多个模式可用 | 连接,但每个候选必须创建相同名字、相同类型和相同绑定方式。1 | 2 | 3 不创建变量;小写裸标识符通常创建新绑定,而不是引用外层同名变量。
可反驳性决定语法位置
能匹配其类型每个可能值的模式是不可反驳模式。let (x, y) = pair 一定成功,所以普通 let、函数参数和 for 绑定可以直接使用它。
可能失败的是 可反驳模式 ,例如 Some(value)、1..=9 或固定长度的切片模式。它必须出现在有失败路径的位置,如 match 分支、if let、while let 或 let-else;let-else 的 else 必须通过 return、break、continue 或发散表达式离开当前路径。
| 形式 | 适合的意图 | 失败时发生什么 | 穷尽检查 |
|---|---|---|---|
match | 多个有意义的情况或需要返回值 | 进入另一个分支 | 是 |
if let | 只处理一个成功形状 | 可选 else | 否 |
while let | 成功时反复取值 | 循环结束 | 否 |
let-else | 失败就提前离开 | else 必须离开 | 针对该模式 |
matches! | 只需要布尔答案 | 返回 false | 否 |
绑定方式与所有权
模式绑定也遵守 Rust 的移动、复制与借用规则。按值匹配拥有型值时,非 Copy 字段默认会被移动,Copy 字段则复制;只移出部分字段会产生 部分移动 ,剩余字段可能还能分别使用,但原结构不能再作为完整值使用。
匹配借用值时, 匹配人体工程学 会在常见结构模式中自动解引用,并让绑定成为引用。match &job { Job { owner, .. } => ... } 中的 owner 因此是借用,而不是从 job 中移出 String。
所有权选择应从匹配对象开始读:match value 允许取得其内容,match &value 只读借用,match &mut value 允许可变借用。ref 和 ref mut 仍可在模式中显式借用字段,但在新代码里,借用整个匹配对象通常更容易审查。
守卫与失败路径
匹配守卫 是分支模式后的 if condition。只有模式先成功,守卫才会求值;守卫为 false 时,匹配继续尝试后面的分支。
守卫能引用该模式创建的绑定,也能读取外部变量。它不把自身条件加入穷尽性证明,所以 Some(x) if x > 0 后面仍要处理其他 Some 值以及 None。
在 A | B if condition 中,守卫作用于整个 A | B,不是只作用于 B。如果候选需要不同条件,应拆成两个分支,让顺序与条件边界都明确。
示例
下面四个例子依次展示穷尽分支、切片解析、借用与移动,以及循环式匹配。所有输出都来自本地 rustc 编译后实际运行的程序。
用枚举封住状态空间
路由函数显式处理每个 Request 变体,并把 match 直接作为返回值。Write 的守卫只处理超限情况,后一个 Write 分支仍负责其余大小。
enum Request {
Health,
Read { id: u32 },
Write { bytes: usize },
}
fn route(request: Request) -> (&'static str, u16) {
match request {
Request::Health => ("health", 200),
Request::Read { id: 0 } => ("invalid id", 400),
Request::Read { .. } => ("read", 200),
Request::Write { bytes } if bytes > 1_024 => ("too large", 413),
Request::Write { .. } => ("write", 202),
}
}
fn main() {
let requests = [
Request::Health,
Request::Read { id: 0 },
Request::Read { id: 7 },
Request::Write { bytes: 2_048 },
];
for request in requests {
let (label, status) = route(request);
println!("{label}: {status}");
}
}health: 200
invalid id: 400
read: 200
too large: 413如果新增 Request::Delete,此函数会因匹配不穷尽而停止编译。若用 _ 取代最后两个显式变体分支,这个提醒便会消失,这正是通配分支有时会削弱演进安全性的原因。
id: 0 是字段子模式,不会绑定 id。下一个分支用 .. 表示该变体的其余字段对当前决定无关,也不会承诺以后新增字段时读取它们。
用切片模式解析命令
split_first() 先把空输入从主路径排除,let-else 让后续代码直接使用 verb 与 args。match 再根据动词和参数切片的长度同时检查命令形状。
#![allow(dead_code)]
#[derive(Debug)]
enum Command<'a> {
Show { id: u32 },
Tag { id: u32, label: &'a str },
}
fn parse_command<'a>(parts: &[&'a str]) -> Result<Command<'a>, String> {
let Some((verb, args)) = parts.split_first() else {
return Err(String::from("empty command"));
};
match (*verb, args) {
("show", [id]) => id
.parse::<u32>()
.map(|id| Command::Show { id })
.map_err(|_| String::from("invalid id")),
("tag", [id, label @ ("urgent" | "normal")]) => id
.parse::<u32>()
.map(|id| Command::Tag { id, label })
.map_err(|_| String::from("invalid id")),
_ => Err(String::from("unknown command")),
}
}
fn main() {
for parts in [
&[][..],
&["show", "17"][..],
&["tag", "17", "urgent"][..],
&["tag", "17", "later"][..],
] {
println!("{:?}", parse_command(parts));
}
}Err("empty command")
Ok(Show { id: 17 })
Ok(Tag { id: 17, label: "urgent" })
Err("unknown command")[id] 只匹配恰好一个参数,[id, label] 只匹配恰好两个参数,因此额外参数不会被静默忽略。label @ ("urgent" | "normal") 同时限制允许值并保留命中的标签。
兜底分支在这里合理,因为输入字符串的状态空间开放,函数的契约就是把未支持的形状归为一种错误。它与封闭的内部枚举不同;是否使用 _ 取决于类型边界与错误策略。
先借用检查,再按值取出
inspect() 匹配 &Job,其中的字段绑定都是借用。主函数随后仍拥有 job,可以把它交给 take_owner(),在那里按值解构并移动 String 字段。
struct Job {
owner: String,
retries: u8,
}
fn inspect(job: &Job) {
match job {
Job {
owner,
retries: attempts @ 1..=3,
} => println!("retry {attempts} for {owner}"),
Job { owner, retries: 0 } => println!("first attempt for {owner}"),
Job { owner, retries } => println!("retry {retries} for {owner}"),
}
}
fn take_owner(job: Job) -> String {
let Job { owner, .. } = job;
owner
}
fn main() {
let job = Job {
owner: String::from("Mina"),
retries: 2,
};
inspect(&job);
println!("owner: {}", take_owner(job));
}retry 2 for Mina
owner: Minaattempts @ 1..=3 既测试闭区间,也保留实际重试次数。因为匹配对象是共享引用,owner 不会移走字符串;inspect() 返回后,该借用结束。
take_owner() 消费整个 Job,所以移动 owner 符合函数契约。若调用方之后仍要使用 job,正确做法通常是让函数接收 &Job 或 &str,而不是为了让旧签名编译就克隆整个值。
在循环中只处理连续成功值
while let 很适合消费返回 Option 或 Result 的状态型操作。这个例子逐个弹出字符串,let-else 跳过解析失败项,再用范围和守卫分类有效数字。
fn main() {
let mut pending = vec!["101", "bad", "42", "7"];
while let Some(text) = pending.pop() {
let Ok(value) = text.parse::<i32>() else {
println!("skip: {text}");
continue;
};
match value {
even @ 0..=100 if even % 2 == 0 => println!("even: {even}"),
odd @ 0..=100 => println!("odd: {odd}"),
outside => println!("outside: {outside}"),
}
}
println!("empty: {}", pending.is_empty());
}odd: 7
even: 42
skip: bad
outside: 101
empty: true向量采用后进先出顺序,所以输出从 "7" 开始。解析失败通过 continue 满足 let-else 必须离开当前路径的要求,而不合法输入是否应跳过,应由调用方契约决定。
第一个分支的守卫只筛选偶数;第二个范围分支仍需接住区间内的奇数。把第二个分支删掉不会让第一个守卫代表整个 0..=100,编译器也不会把任意 % 条件纳入覆盖证明。
陷阱
可反驳性不是运行时猜测
编译器从类型和模式形式判断一个模式是否可能失败。变量绑定、只有变量的元组解构以及字段齐全的单一结构体模式通常不可反驳;枚举变体、字面值、范围和限定长度的切片模式通常可反驳。
同一段模式换到不同语法位置,要求也随之变化。普通 let 没有失败分支,只接受不可反驳模式;if let 接受失败并选择是否运行代码块;let-else 接受失败,但要求失败路径离开,以保证后续绑定一定已初始化。
if let 使用不可反驳模式通常会产生警告,因为条件永远为真。它虽然可能编译,却隐藏了作者原本想测试什么;把它改成普通 let,或恢复真正可失败的子模式。
let-else 的作用域
let PATTERN = expression else { ... }; 成功后,模式创建的绑定进入后续外层作用域。这与 if let 不同,后者的绑定只存在于成功代码块中。
else 必须发散,才能保证控制流到达下一句时模式已经成功。返回错误、continue 当前循环或调用返回 ! 的函数都可以;只记录日志然后落回主路径不行。
复杂验证不必全部塞进一个模式。模式适合表达类型结构,业务谓词适合普通条件或返回 Result 的验证函数;边界分开后,失败原因也更容易保留。
匹配人体工程学与部分移动
模式作用于位置,而不仅是临时数值。读取 match value 时,分支可以取得 value 内部字段的所有权;读取 match &value 时,自动解引用让结构模式仍保持简洁,但默认绑定成为共享引用。
这种自动调整就是匹配人体工程学。它减少了层层 & 与 ref,但不会改变底层所有权规则;不确定绑定类型时,可以加一个局部类型标注,或让编辑器显示推断类型,而不是根据变量拼写猜测。
按值模式可以混合复制与移动。若结构含 u32 与 String,绑定前者会复制,绑定后者会移动;未移动的字段仍可单独访问,但对整个结构调用方法通常需要完整值,因此会被拒绝。
借用整个匹配对象通常可避免部分移动。另一种精确写法是在字段模式上用 ref name 或 ref mut name,但它混合了多种绑定方式,审查者必须逐字段追踪;除非只需借用少数字段,否则优先让函数签名和匹配对象表达借用意图。
实现 Drop 的类型不能随意通过模式移出字段,因为析构函数需要接收完整的 &mut self。需要有意取出资源时,可消费整个值的专用方法,或把字段放进 Option 并用 take() 留下合法状态。
或模式、@ 绑定与守卫
或模式 left | right 表示候选集合的并集。所有候选必须创建相同绑定,否则分支表达式无法获得一组稳定的局部变量;绑定类型或借用方式不同也会被拒绝。
@ 的左侧创建绑定,右侧继续约束同一个匹配部分。id @ 1..=9 比仅写范围多保留了实际 id;嵌套结构中也能绑定整个子对象,同时检查其字段。
优先级容易误读。守卫写在整个分支模式之后,因此 A | B if ready 等价于 (A | B) if ready;若只有 B 需要 ready,应写成两个分支。
守卫为普通表达式,可以调用函数、读取外部状态,甚至产生副作用。副作用会让分支选择依赖求值时机,也会使测试更难;守卫最好保持短小且无副作用,复杂决策移到命名清楚的函数中。
穷尽性、可达性与 API 边界
穷尽性保证每个类型层面的可能值都能选到一个分支,不保证每个分支的业务行为正确。_ => Ok(()) 能通过检查,却可能把未处理事件报告成成功;覆盖完整和语义完整必须分别审查。
可达性从上到下计算。变量模式会匹配其类型的任何值,所以放在前面时尤其危险;宽范围也可能完全覆盖后面的窄范围。警告不应被机械禁止,因为它通常揭示模式顺序或命名错误。
公开 API 的枚举可使用 #[non_exhaustive] 要求其他 crate 的调用方保留兜底分支。此时通配分支是兼容性契约的一部分,但它仍应返回明确错误或保守行为,而不是默认 unreachable!()。
对字符串、整数和外部协议字段,输入空间本来就是开放的。此类匹配需要安全兜底;对自己控制的封闭枚举,显式变体通常更能利用编译器帮助完成重构。
选择最窄但不丢信息的形式
match 最适合多个分支都影响结果,或枚举的每个变体都应得到明确处理。它也是表达状态转换的自然位置,因为输入状态与事件可以组成一个元组一起匹配。
if let 适合只关心一个形状,而且失败确实无需动作。添加 else 后若两边都很重要,match 往往更清楚,也能在类型变化时提供穷尽提醒。
let-else 适合把失败路径提前移走,让主路径保持平直。它不负责描述多个恢复策略;失败原因需要区分时,应匹配 Result 或使用 ? 保留错误信息。
while let 适合某个状态型操作连续成功时循环,例如反复 pop() 或接收通道消息。它会把首次不匹配解释为正常终止,所以错误与结束共享一个 Err 时,可能需要显式 match 才不会吞掉错误。
matches!(value, pattern if guard) 只返回布尔值,不提供绑定给后续代码。它适合过滤与断言;若紧接着还要从同一值中取数据,再匹配一次通常是重复工作,应直接使用 match 或 if let。
延伸阅读
4个问题 · 1 道输出预测题 · 1 道找错题