课程概览 · 第 9 章
上一篇:枚举:把状态、缺失和失败建模为类型
下一篇:泛型与 trait:复用行为,同时保持接口清晰
模式匹配把数据形状、控制流和字段绑定写在一起。穷尽 match 能在枚举新增变体时提醒处理遗漏;按引用还是按值匹配,则决定绑定借用还是移走字段。这两条边界应分别判断,而不是把所有匹配都理解为消费。
图:覆盖范围决定控制流,匹配对象和绑定方式决定所有权。前置知识与学习目标
需要第 6–8 章的借用、结构体和枚举。学完后应能:
- 用无通配分支的穷尽
match 保护核心状态处理的演进。 - 预测匹配引用与匹配值时字段的绑定类型。
- 按需要选择
if let、let else、while let。 - 组合
@、范围和守卫,并识别 Edition 2024 的绑定限制。
所有源码仅依赖标准库、Edition 2024。独立示例各自含定义和入口,不依赖第 8 章实际文件;组合反例明确说明替换点。
穷尽 match 与枚举演进
match 是表达式,分支结果需要汇合为共同类型,允许发散分支等类型强制转换。核心业务状态不写 _,让新增状态成为一次显式复核,而不是静默落进兜底。
独立示例(标准库;完整文件 status_text.rs):主例穷尽处理全部状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| #[derive(Debug)] enum TaskState { Todo, InProgress { started_at: u64 }, Done { finished_at: u64 }, Cancelled { reason: String }, }
fn status_text(state: &TaskState) -> String { match state { TaskState::Todo => String::from("todo"), TaskState::InProgress { started_at } => { format!("in progress since t={started_at}") } TaskState::Done { finished_at } => { format!("done at t={finished_at}") } TaskState::Cancelled { reason } => { format!("cancelled: {reason}") } } }
fn main() { assert_eq!(status_text(&TaskState::Todo), "todo"); assert_eq!( status_text(&TaskState::InProgress { started_at: 100 }), "in progress since t=100" ); assert_eq!( status_text(&TaskState::Done { finished_at: 250 }), "done at t=250" ); let cancelled = TaskState::Cancelled { reason: String::from("duplicate"), }; assert_eq!(status_text(&cancelled), "cancelled: duplicate"); }
|
故意失败:编译错误(标准库;在 status_text.rs 中只用下块替换 TaskState 定义,保留函数和 main):新增状态但未迁移处理器。
1 2 3 4 5 6 7 8
| #[derive(Debug)] enum TaskState { Todo, InProgress { started_at: u64 }, Done { finished_at: u64 }, Cancelled { reason: String }, Paused { since: u64 }, }
|
失败原因是 status_text 的模式不穷尽。若原来有 _,它会继续编译,但不代表新状态处理正确。第 8 章 start、finish 同样需要在新增状态时复核,没有本课程未定义的 cancel 方法。外部 crate 的 #[non_exhaustive] 枚举是例外:使用方通常必须保留通配分支,见第 15 章。
默认绑定模式:按引用与按值
匹配引用时,编译器按匹配人体工程学规则调整默认绑定模式,常见字段绑定自动变成引用。匹配拥有值也不是一律移动所有字段:只移动真正被按值绑定的非 Copy 部分,_ 不会把字段绑定出来。
独立示例(标准库;完整文件 borrowed_pattern.rs):按引用拆出字段,随后继续使用原值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| #[derive(Debug)] struct Task { id: u64, title: String, state: TaskState }
#[derive(Debug)] enum TaskState { Todo, Cancelled { reason: String } }
fn main() { let task = Task { id: 7, title: String::from("ship release notes"), state: TaskState::Cancelled { reason: String::from("duplicate of #3"), }, }; match &task { Task { id, title, state: TaskState::Cancelled { reason } } => { assert_eq!(*id, 7); assert_eq!(title, "ship release notes"); assert_eq!(reason, "duplicate of #3"); } Task { state: TaskState::Todo, .. } => unreachable!(), } assert_eq!(task.title, "ship release notes"); println!("{task:?}"); }
|
这里 id 是 &u64,title、reason 是 &String。只读场景优先匹配引用,避免无意获取所有权;若业务要取走数据,按值匹配是正确工具。
故意失败:编译错误(标准库;沿用 borrowed_pattern.rs 的两个类型,只用下块替换 main):按值移出标题后再读标题。
1 2 3 4 5 6 7 8 9 10
| fn main() { let task = Task { id: 7, title: String::from("ship release notes"), state: TaskState::Todo, }; let Task { title, .. } = task; assert_eq!(title, "ship release notes"); println!("{}", task.title); }
|
失败是使用已经移动的字段,不是非穷尽问题。显式 ref 也可以借用字段,但常见只读解构直接让右侧成为引用更清晰。
if let 与 let else
只关心一个形状、其余情况无动作时用 if let。如果两个分支都有意义,可以写 if let ... else;多状态业务分派仍适合完整 match。
独立示例(标准库;完整文件 if_let.rs):观察可选配置,不取走内部文本。
1 2 3 4 5 6 7
| fn main() { let title = Some(String::from("ship release notes")); if let Some(text) = &title { assert_eq!(text, "ship release notes"); } assert!(title.is_some()); }
|
let else 让失败路径提前离开,成功后的绑定在当前作用域继续可用。else 必须发散,例如 return、break、continue 或 panic!;普通值表达式不能保证失败后变量已初始化。
独立示例(标准库;完整文件 let_else.rs):解析 CLI 风格输入,示例只校验格式、数值和空标题,不复用第 7 章完整领域规则。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #[derive(Debug, PartialEq)] struct Task { id: u64, title: String }
fn parse_task(raw: &str) -> Result<Task, String> { let Some((id_part, title_part)) = raw.split_once(':') else { return Err(format!("invalid task format: {raw:?}")); }; let Ok(id) = id_part.trim().parse::<u64>() else { return Err(format!("task id is not a number: {id_part:?}")); }; if title_part.trim().is_empty() { return Err(String::from("task title is empty")); } Ok(Task { id, title: title_part.trim().to_string() }) }
fn main() { let task = parse_task("7: ship release notes").unwrap(); assert_eq!(task.id, 7); assert_eq!(task.title, "ship release notes"); assert_eq!( parse_task("no-colon-here"), Err(String::from("invalid task format: \"no-colon-here\"")) ); assert_eq!( parse_task("seven: title"), Err(String::from("task id is not a number: \"seven\"")) ); assert_eq!(parse_task("7: "), Err(String::from("task title is empty"))); }
|
while let 与所有权
while let 只是重复匹配条件表达式,是否消费元素取决于表达式。本例调用 pop_front,每次返回移出的元素;队列清空来自这个操作,不来自 while let 本身。
独立示例(标准库;完整文件 queue.rs):FIFO 队列消费和借用迭代对照。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| use std::collections::VecDeque;
fn main() { let mut queue = VecDeque::from([ String::from("ship release notes"), String::from("write postmortem"), String::from("review pull requests"), ]); let mut processed = Vec::new(); while let Some(item) = queue.pop_front() { processed.push(item); } assert_eq!(processed, [ "ship release notes", "write postmortem", "review pull requests", ]); assert!(queue.is_empty());
let mut iter = processed.iter(); let mut count = 0; while let Some(item) = iter.next() { assert!(!item.is_empty()); count += 1; } assert_eq!(count, 3); assert_eq!(processed.len(), 3); }
|
第二个循环消费迭代器状态,但只借用元素,原容器保留。for item in &values 借用,for item in values 对常见拥有容器调用消费式 IntoIterator;不能说 for 必借用。迭代 API 在第 12 章展开。
@、范围与守卫
@ 在匹配子模式的同时保留绑定;范围模式匹配整数或字符的范围;守卫在模式匹配后增加布尔条件。守卫失败会继续尝试后面的分支,不能用它替代无条件覆盖变体的穷尽分支。
代码片段(标准库;沿用 status_text.rs 的 TaskState,把下块放到该文件模块级,并用此 main 替换原 main):优先级与分支顺序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| fn priority(state: &TaskState) -> &'static str { match state { TaskState::InProgress { started_at: t @ ..=50 } => { assert!(*t <= 50); "legacy" } TaskState::InProgress { started_at } if *started_at >= 900 => { "urgent" } TaskState::InProgress { .. } => "normal", TaskState::Todo => "queued", TaskState::Done { .. } => "none", TaskState::Cancelled { .. } => "none", } }
fn main() { assert_eq!( priority(&TaskState::InProgress { started_at: 30 }), "legacy" ); assert_eq!( priority(&TaskState::InProgress { started_at: 950 }), "urgent" ); assert_eq!( priority(&TaskState::InProgress { started_at: 100 }), "normal" ); assert_eq!(priority(&TaskState::Todo), "queued"); assert_eq!(priority(&TaskState::Done { finished_at: 999 }), "none"); }
|
这里“历史任务”阈值包含 50,不是严格早于 50。t 是引用;需要数值比较时解引用。守卫应保持含义清晰,昂贵计算不应不经思考地重复放进多个分支。
深入阅读:Edition 2024 的显式绑定模式
Edition 2024 的 match ergonomics 变化限制在已经隐式进入引用绑定模式的子模式中使用 mut、ref、ref mut 等修饰。前面的普通 match &task 没有混用这些修饰,不受此限制。
故意失败:编译错误(标准库;完整文件 implicit_mut.rs;Edition 2024):默认绑定模式已经隐式变成引用。
1 2 3 4 5
| fn main() { let [mut x] = &[1]; x += 1; assert_eq!(x, 2); }
|
错误类别是该 edition 下不允许此位置的 mut 绑定修饰,不应归因于数组借用必定不能解构。
独立示例(标准库;完整文件 explicit_mut.rs):显式写出引用模式后绑定可变局部副本。
1 2 3 4 5
| fn main() { let &[mut x] = &[1]; x += 1; assert_eq!(x, 2); }
|
这里 x 是复制出的 i32,不是通过共享引用修改数组。完全显式模式把引用层拆开,避免依赖隐式默认绑定再添加不兼容修饰。
自测
- 无
_ 的 status_text 遇到新增 Paused 有何结果?外部 non_exhaustive 枚举为何不同? - 本章
match &task 中 title 的类型是什么?按值绑定它有什么后果? - 什么时候使用
if let,let else 的失败分支有什么要求? while let 必然消费容器吗?for 必然只借用吗?- 守卫失败后怎样继续匹配?显式
[mut x] 修正例是否修改原数组?
参考答案
- 编译器报告模式不穷尽,要求复核新增状态;外部非穷尽枚举允许未来添加变体,使用方需要通配处理。
&String,原值仍可用;按值绑定 String 会移动字段,不能随后再读取该字段。- 只关心一个模式时用
if let;let else 的失败分支必须离开当前作用域或不返回。 - 都不是;看条件表达式、匹配对象以及
IntoIterator 的实现。 - 继续后续分支;显式引用模式中的
x 是整数副本,不修改数组。