课程概览 · 第 9 章

上一篇:枚举:把状态、缺失和失败建模为类型

下一篇:泛型与 trait:复用行为,同时保持接口清晰

模式匹配把数据形状、控制流和字段绑定写在一起。穷尽 match 能在枚举新增变体时提醒处理遗漏;按引用还是按值匹配,则决定绑定借用还是移走字段。这两条边界应分别判断,而不是把所有匹配都理解为消费。

穷尽 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,不是通过共享引用修改数组。完全显式模式把引用层拆开,避免依赖隐式默认绑定再添加不兼容修饰。

自测

  1. 无 _ 的 status_text 遇到新增 Paused 有何结果?外部 non_exhaustive 枚举为何不同?
  2. 本章 match &task 中 title 的类型是什么?按值绑定它有什么后果?
  3. 什么时候使用 if let,let else 的失败分支有什么要求?
  4. while let 必然消费容器吗?for 必然只借用吗?
  5. 守卫失败后怎样继续匹配?显式 [mut x] 修正例是否修改原数组?

参考答案

  1. 编译器报告模式不穷尽,要求复核新增状态;外部非穷尽枚举允许未来添加变体,使用方需要通配处理。
  2. &String,原值仍可用;按值绑定 String 会移动字段,不能随后再读取该字段。
  3. 只关心一个模式时用 if let;let else 的失败分支必须离开当前作用域或不返回。
  4. 都不是;看条件表达式、匹配对象以及 IntoIterator 的实现。
  5. 继续后续分支;显式引用模式中的 x 是整数副本,不修改数组。