课程概览 · 第 9 章
上一篇:枚举:把状态、缺失和失败建模为类型
下一篇:泛型与 trait:复用行为,同时保持接口清晰
模式匹配不仅是更好看的条件判断。它能把枚举、结构体和嵌套数据拆开,同时要求代码处理所有状态,是 Rust 业务代码安全演进的关键工具。第 8 章的状态机把非法转移挡在了方法里,本章负责消费这些状态:把每个 TaskState 变成可显示的文本、可执行的分支,并且保证新增状态时编译器会逼你审查每处处理代码。
图:完整 match 守住状态演进;更窄的模式语法解决单分支、消费和早退,解构方式决定所有权。 学习目标与默认选择 学完本章你应当能够:
用穷尽的 match(无 _)把 TaskState 转换为显示文本,并解释为什么这是演进保护网。 区分按值匹配与按引用匹配(match &task),避免无意移动 String、Vec 字段。 在正确场景选择 if let、while let、let else、@ 绑定、范围模式与匹配守卫。 默认选择:处理完整状态机用 match 且不写 _;只关心一个分支用 if let;失败即退出用 let else。_ 通配留给真正"其余情况语义相同"的场景(如日志),核心业务分支不要用它兜底。
穷尽 match:状态到文本 match 是表达式,所有分支返回同一类型。核心状态机的 match 不写 _,让新增变体直接变成编译错误:
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 39 #[derive(Debug)] pub 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" ); assert_eq! ( status_text (&TaskState::Cancelled { reason: String ::from ("duplicate" ) }), "cancelled: duplicate" ); }
匹配引用时,绑定自动成为引用:started_at 是 &u64、reason 是 &String,直接用于 format! 即可。这里没有任何字段被移动。
为什么不用 _ 假设给 TaskState 新增一个 Paused { since: u64 } 变体:
穷尽的 match 立即编译失败:non-exhaustive patterns,错误信息精确指出缺 TaskState::Paused { .. }。你被迫停下来,决定暂停中的任务该显示什么、能否被取消、能否完成。 带 _ 的 match 继续编译,新状态静默落进兜底分支,显示成错误的文本、走错误的流程。 编译错误就是演进保护网:新增变体迫使你审查所有状态处理器 。丢掉 _,等于丢掉这张网。
引用模式:匹配之后数据仍可用 match &task 只借用:模式绑定的是引用,原值保持完整。对比按值匹配会移动非 Copy 字段:
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 #[derive(Debug)] pub struct Task { pub id: u64 , pub title: String , pub state: TaskState, }#[derive(Debug)] pub enum TaskState { Todo, InProgress { started_at: u64 }, Done { finished_at: u64 }, 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" ); } _ => unreachable! (), } assert_eq! (task.title, "ship release notes" ); println! ("{task:?}" ); }
若把 match &task 换成 match task,title 和 reason 会被移动出去,task 变成部分移动状态,最后的 assert_eq!(task.title, ...) 就无法编译。只读场景一律匹配引用。
if let:只关心一个分支if let 适合"命中一个模式就做事,其余情况确实没有动作":
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #[derive(Debug)] pub enum TaskState { Todo, InProgress { started_at: u64 }, Done { finished_at: u64 }, Cancelled { reason: String }, }fn main () { let state = TaskState::InProgress { started_at: 100 }; if let TaskState ::InProgress { started_at } = &state { assert_eq! (*started_at, 100 ); println! ("running since t={started_at}" ); } if let TaskState ::Done { .. } = &state { unreachable! ("state is InProgress" ); } }
判断标准很简单:如果 else 分支也有业务含义,回到完整 match;if let 只在"其余情况统一无动作"时使用。
while let:消费队列while let 在循环条件里反复匹配,天然适合逐项消费队列:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 use std::collections::VecDeque;fn main () { let mut queue : VecDeque<&str > = VecDeque::new (); queue.push_back ("ship release notes" ); queue.push_back ("write postmortem" ); queue.push_back ("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 ()); }
pop_front 返回 Option:Some 拿走一个元素的所有权,None 表示队列已空。FIFO 顺序由此保证。
let else:守卫式早退模式不匹配就离开当前作用域,成功路径保持在最左侧,避免层层嵌套:
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 39 40 41 42 43 44 45 #[derive(Debug, PartialEq)] pub struct Task { pub id: u64 , pub 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 (format! ("task title is empty" )); } Ok (Task { id, title: title_part.trim ().to_string (), }) }fn main () { let ok = parse_task ("7: ship release notes" ).unwrap (); assert_eq! (ok.id, 7 ); assert_eq! (ok.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" )) ); }
else 分支必须离开当前作用域(return、break、continue 或 panic!),编译器强制这一点,所以 let 之后主流程可以放心使用解出的值。
@ 绑定、范围模式与匹配守卫@ 让你在匹配子结构的同时绑定整个值;范围模式匹配数值区间;守卫给分支加布尔条件:
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 #[derive(Debug)] pub enum TaskState { Todo, InProgress { started_at: u64 }, Done { finished_at: u64 }, Cancelled { reason: String }, }fn priority (state: &TaskState) -> &'static str { match state { TaskState::InProgress { started_at: t @ ..=50 } => { let _ = t; "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" ); }
三点注意:
@ 绑定 (t @ ..=50):既测试范围,又把值绑定到 t,两步合一。范围模式 (..=50、900..):只适用于整数和字符这类可枚举范围。守卫 (if *started_at >= 900):给分支附加运行时条件;守卫失败时匹配继续尝试后面的分支,且守卫不能破坏穷尽性–priority 的核心 TaskState 分支依然全部列出。穷尽性是演进保护网 把本章所有工具放回状态机的语境:第 8 章的 start / finish / cancel 用 match 分派每个变体,本章的 status_text / priority 用 match 消费每个变体。它们共同的特点是没有 _ 。
将来给 TaskState 加 Paused { since: u64 } 时,编译器会把这些 match 全部标红。被标红的每一处都是一个真实的产品决策点:暂停的任务显示什么?能直接完成吗?优先级算多少?穷尽性检查把这些决策从"上线后才发现漏了一个分支"提前到编译时刻。这就是"用穷尽分支处理业务状态"的含义:新增变体的成本是一次编译错误 + 一次有意识的审查,而不是一次生产事故。
边界与失败场景 按值匹配会移动字段。 match task(无 &)把非 Copy 字段移进分支绑定;只读场景匹配 &task。守卫不参与穷尽性。 if 条件失败只是"这个分支不适用",继续匹配后续分支;不要用守卫承担本该由模式变体表达的区分。let else 必须发散。 else 分支只能 return / break / continue / panic!,写普通表达式无法编译。while let 会消费数据。 匹配的是 pop_front 这类取走所有权的 Option;只想遍历不改容器时用 for 循环。为什么可行 match 在编译期检查两件事:是否有遗漏的可能值,以及前面的分支是否已覆盖后面不可达的分支。枚举的变体集合在编译期完全已知,所以穷尽性可以精确判定;Option/Result 也一样–这就是第 8 章"调用方必须处理失败"在机制上的来源。
绑定模式的类型推导同样精确:匹配 &TaskState 时,模式里的 reason 自动是 &String;匹配 TaskState(按值)时是 String。模式在"拆数据"的同时完成了"决定数据怎么流动",这也是第 6 章所有权规则在语法层的直接体现。
常见误区 核心状态机用 _ 兜底。 新增变体静默落进兜底分支,穷尽性保护网失效;兜底只用于"其余情况语义确实相同"的场合。只读场景按值匹配。 match task 移动 title、reason 等字段,后续使用原值时才发现;先想清楚要不要保留原值。用 if let 表达双分支业务。 else 也有含义时用 match 或 if let ... else,不要写成两个互补的 if let。在守卫里做重活。 守卫应是廉价的布尔判断;需要复杂逻辑时先收敛数据形态再匹配。while let 与 for 混淆。 前者逐项消费(容器被清空),后者只借用遍历;按是否要保留数据选择。自测 status_text 里删掉 TaskState::Cancelled 分支会发生什么?答案方向: 编译错误 non-exhaustive patterns,指出缺 Cancelled { .. };这是穷尽性检查在阻止你遗漏状态处理器。match &task 与 match task 的绑定类型有什么区别?答案方向: 前者绑定 &String、&u64(借用,原值完整);后者绑定 String、u64(非 Copy 字段被移动,原值部分失效)。什么时候选 if let 而不是 match?答案方向: 只关心一个变体、其余情况确实无动作时;else 有业务含义就回到 match。 let else 的 else 分支有什么限制?为什么?答案方向: 必须发散(return/break/continue/panic);否则解构失败后变量未初始化却被后续代码使用,编译器用这条规则保证主流程变量一定有值。匹配守卫失败后,匹配去向哪里?答案方向: 继续尝试后续分支,不会整个 match 失败;守卫只是给单个分支附加条件,不改变穷尽性结构。 下一篇把这些具体类型抽象成可复用的接口:泛型与 trait:复用行为,同时保持接口清晰 。