📚 Rust 课程系列

  1. 课程概览
  2. 基础语法(一):变量、数据类型与字符串
  3. 切片(Slice):序列的借用视图
  4. 基础语法(二):运算符、表达式与控制流
  5. 函数与输入输出
  6. 所有权、借用与生命周期
  7. 结构体
  8. 枚举
  9. 模式匹配(本文)
  10. 类型系统:泛型、trait 与多态
  11. 集合与容器
  12. 错误处理与 Panic 恢复
  13. 模块、属性与宏
  14. 智能指针、迭代器与闭包
  15. 并发与异步编程
  16. Unsafe Rust 与常用 trait 详解
  17. 工具链、Cargo 与外部 crate
  18. 最佳实践、性能与调试

模式匹配远不止是 C/Java 里 switch 的升级版——switch 只能对标量值做相等比较,而 Rust 的模式匹配能同时完成「判断」与「拆解」两件事:一边判断一个值属于枚举的哪个变体,一边把这个变体内部的数据解构出来绑定到变量。配合穷尽性检查,编译器会逼着你处理每一种可能,让「漏写一个分支」这类 bug 在编译期就暴露无遗。

本章承接《枚举》——枚举定义了「互斥的几种可能」,而模式匹配就是处理这些可能的惯用手段。

模式匹配是什么:从「判断」到「拆解」

在很多语言里,「判断一个值是哪种情况」和「从值里取出数据」是两件分开的事。以 TypeScript 为例:

1
2
3
4
5
6
7
8
9
10
11
12
// TS:先判断 kind,再用 as 断言取字段
type Shape =
| { kind: "circle"; r: number }
| { kind: "rect"; w: number; h: number };

function area(s: Shape): number {
if (s.kind === "circle") {
return Math.PI * s.r * s.r; // 这里 s.r 靠类型收窄
} else {
return s.w * s.h;
}
}

这里「判断」用 if (s.kind === ...),「取数据」靠 TS 的类型收窄(narrowing)隐式完成。Rust 把这两步合并成一个语法动作——模式既描述了「值长什么样」,又声明了「把哪些部分绑定到哪些变量」:

1
2
3
4
5
6
7
8
9
10
11
12
enum Shape {
Circle { r: f64 },
Rect { w: f64, h: f64 },
}

fn area(s: &Shape) -> f64 {
match s {
// 同时完成判断与解构:r 自动绑定
Shape::Circle { r } => 3.14 * r * r,
Shape::Rect { w, h } => w * h,
}
}

Shape::Circle { r } 这个模式既匹配 Circle 变体,又把内部的 r 字段绑定到同名变量。这就是模式匹配的核心:一个模式 = 一个形状断言 + 一组绑定声明

💡 提示:模式匹配的「判断 + 拆解合一」并非 Rust 首创,它继承自 ML/Haskell/OCaml 的函数式传统。Rust 的独到之处在于把它和所有权、借用深度耦合——模式不仅能解构值,还能决定是「移动」还是「借用」内部数据,这既是威力来源,也是陷阱高发区。

match:穷尽式匹配的基石

match 是 Rust 模式匹配的主力构造。它有四条核心规则:是表达式、必须穷尽、各分支类型一致、按顺序求值。

match 是表达式

与 C/Java 的 switch(语句)不同,match表达式,它会求值出一个结果。这意味着 match 可以出现在任何期望值的位置——赋值右侧、函数返回值、函数参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}

// match 直接作为函数返回值
fn value(c: Coin) -> u32 {
match c {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}

// match 求值的结果赋给变量
let cents = value(Coin::Dime);

因为 match 求值,所以分支末尾不写逗号以外的结束符,也不能用 break。每个分支 => 右侧是一个表达式,求值后即成为该分支的结果。需要执行多条语句时用代码块,块的最后一行作为值:

1
2
3
4
5
6
7
8
9
10
fn describe(c: Coin) -> &'static str {
match c {
Coin::Penny => {
println!("幸运的一分钱");
"1 分" // 块的尾表达式,无分号
}
Coin::Quarter => "25 分",
_ => "其他",
}
}

🔄 对比:C/Java 的 switch 是语句,不能直接求值;C# 8.0+ 的 switch 表达式才追上了这一点。Python 3.10 的 match 语句也偏向语句式,需手动赋值。Rust 从一开始就让 match 是表达式,这和它「面向表达式」的语法哲学一致(见《基础语法(二)》)。

穷尽性检查

match 必须覆盖被匹配类型的所有可能,否则编译失败。这是 Rust 替代「漏写 default 分支」这一类 bug 的根本机制:

1
2
3
4
5
6
7
8
9
10
// 编译失败:漏掉了 Quarter
// error[E0004]: non-exhaustive patterns
fn bad(c: Coin) -> u32 {
match c {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
// Quarter 未处理
}
}

当你后来给 Coin 新增一个变体 Dollar,所有 match Coin 的地方都会编译失败——编译器会一一列出未覆盖的位置。这种「破坏性变更在编译期可见」的特性,是枚举 + match 组合的最大价值。

穷尽性对布尔值也成立:match bool { true => .. } 会因为漏掉 false 而报错。对没有 | 合并的整数字面量则无法穷尽,必须用 _ 或范围兜底:

1
2
3
4
5
6
7
8
let n = 3;
match n {
0 => println!("零"),
1 => println!("一"),
2 => println!("二"),
// 整数无法穷尽,必须有 _ 或覆盖剩余
_ => println!("其他"),
}

⚠️ 注意_ 通配符会吞掉所有未列出的情况。一旦写了 _,未来新增枚举变体时编译器不再提醒你。所以对自家枚举,优先显式列出每个变体;只在真正「其余情况等价」时才用 _,并且最好在旁边留一行注释说明意图。

各分支返回类型必须一致

既然 match 求值出一个结果,所有分支的结果类型必须相同——否则编译器不知道整个 match 该是什么类型:

1
2
3
4
5
6
7
8
9
// 编译失败:分支类型不一致
// expected `&str`, found integer
fn broken(c: Coin) {
let v = match c {
Coin::Penny => 1, // i32
Coin::Nickel => "5 分", // &str,类型冲突
_ => 0,
};
}

需要返回「不同类型的结果」时,通常说明你该用一个枚举把可能性编码进类型,而不是靠动态分发。例如解析器返回不同 AST 节点,正是枚举的用武之地。

分支类型一致是静态要求,与运行时实际走哪个分支无关。即便某个分支在运行时永远走不到,它的类型仍参与推断。

守卫条件

守卫(guard)给分支附加一个布尔过滤,让你在「形状匹配」之外再加一层「值条件」:

1
2
3
4
5
6
7
8
9
let n = Some(4);
match n {
// 形状匹配 Some(x),再要求 x 是偶数
Some(x) if x % 2 == 0 => {
println!("偶数: {}", x);
}
Some(x) => println!("奇数: {}", x),
None => println!("无"),
}

守卫是 if 后跟一个 bool 表达式。它让一个模式可以「匹配但被守卫拒绝」——此时该分支不算命中,继续尝试下一个分支。这带来一个微妙但关键的后果:带守卫的分支不再保证穷尽性。编译器无法静态判断守卫是否覆盖所有情况,因此下例能编译:

1
2
3
4
5
6
7
8
9
fn sign(n: Option<i32>) -> i32 {
match n {
Some(x) if x > 0 => 1,
Some(x) if x < 0 => -1,
// 没有 Some(0) 的分支,也没有 _
// 但编译器认为守卫可能都不满足,故放行
_ => 0, // 实际仍需要兜底
}
}

若去掉最后的 _,编译会失败——因为 Some(0) 不满足任何守卫,落入未覆盖。但编译器的判断是「所有带守卫的分支都可能不命中」,所以你必须补一个兜底。守卫的更多限制见后文「守卫条件进阶」一节。

分支顺序与默认分支 _

match 按从上到下的顺序尝试匹配,第一个命中的分支胜出。因此更具体的模式必须写在更通用的模式之前:

1
2
3
4
5
6
7
8
let x = 4;
match x {
// 具体分支在前
0 => println!("零"),
// 范围在后(若颠倒,0 会被范围吞掉)
1..=9 => println!("个位"),
_ => println!("其他"),
}

_ 是「通配符」模式,匹配任何值且不绑定变量。它通常放在最后作为默认分支。Rust 编译器会警告「_ 之后的分支永远不会命中」(unreachable_patterns):

1
2
3
4
match 5 {
_ => println!("全部"),
5 => println!(), // 警告:不可达
}

除了 _,可用「或模式」| 列出多个具体值作为一组。但 _ 仍是覆盖剩余的唯一通配手段。

💡 提示:写默认分支时,与其用 _ => {} 静默忽略,不如用 _ => unreachable!("说明原因")_ => panic!(...) 显式声明「这里不该发生」。这样若未来枚举扩展意外落入此分支,能在运行时立刻暴露,而不是悄无声息地跳过。

match 的求值与绑定作用域

match 求值时,只有被命中的那个分支的 => 右侧会被执行,其余分支的表达式完全不求值。分支内绑定的变量只在该分支的作用域内有效:

1
2
3
4
5
6
let opt: Option<i32> = Some(10);
let doubled = match opt {
Some(x) => x * 2, // x 仅在此分支可见
None => 0,
};
// 这里 x 已不在作用域

分支之间互不影响:一个分支里 move 走的值不会影响其他分支的可用性,因为只有命中分支才真正执行 move。

if let 与 while let:放弃穷尽性的轻量选择

match 强大但有时过重——当你只关心一种情况、其余全忽略时,写一整张 match 表加 _ => {} 显得啰嗦。if letwhile let 就是为这种场景准备的快捷语法。

if let:单一模式匹配

if let 把「匹配一种模式,否则走 else」浓缩成一行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let config: Option<&str> = Some("8080");

// if let:只关心 Some
if let Some(port) = config {
println!("端口: {}", port);
} else {
println!("未配置端口");
}

// 等价的 match 写法更冗长
match config {
Some(port) => println!("端口: {}", port),
_ => println!("未配置端口"),
}

if let PATTERN = EXPR { ... }PATTERN 匹配 EXPR 时执行块,绑定的变量在块内可用。else 块可选,在未匹配时执行。

if let 也可以链式 else if let,构成多路「试探」:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum Event {
Key(char),
Click(u32, u32),
Quit,
}

fn handle(e: Event) {
if let Event::Key(c) = e {
println!("按键 {}", c);
} else if let Event::Click(x, y) = e {
println!("点击 ({}, {})", x, y);
} else if let Event::Quit = e {
println!("退出");
}
}

不过这种写法放弃了穷尽性——未来给 Event 加变体,编译器不会提醒。此时用 match 更安全。

⚠️ 注意if let 最大的代价是放弃穷尽性检查。当被匹配的类型是自家枚举且变体可能增加时,优先用 match,让编译器替你盯着新增分支。只有匹配外部类型(如 Option/Result,变体稳定不会变)或确实只关心单一情况时,if let 才是合适的选择。

while let:逐项消费

while letif let 的循环版:每次迭代先尝试匹配,成功则执行循环体并继续,失败则退出循环。它天然适合「逐项消费」迭代器或栈结构:

1
2
3
4
5
6
// 逐个弹出栈顶,直到空
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("弹出: {}", top);
}
// 循环结束时 stack 已空

while let Some(_) = iter.next()for item in iter 在效果上接近,但 while let 更灵活——你可以控制消费的步长,或在循环体里修改被消费的数据源:

1
2
3
4
5
6
7
8
// 只消费偶数,奇数跳过
let mut it = (1..=10).into_iter();
while let Some(n) = it.next() {
if n % 2 == 0 {
println!("偶 {}", n);
}
// 奇数不做处理,next 自然推进
}

💡 提示while letfor 的取舍——for 适用于「遍历全部」;while let 适用于「按条件消费,可能提前结束或需要手动控制迭代」。注意 while let Some(x) = col.pop()消费集合(取出所有权),而 for x in &col 只是借用。

何时该用 if let 而非 match

下表归纳三种构造的取舍:

构造穷尽性返回值典型场景
match强制穷尽是(表达式)多路分发、自家枚举
if let放弃否(语句)只关心一种变体
while let放弃否(语句)逐项消费、循环匹配
flowchart TD
  Q[需要匹配枚举?] -->|是| A{关心几种变体?}
  Q -->|否, 只是 Option/Result| B{只关心一种?}
  A -->|全部| M[用 match: 穷尽安全]
  A -->|一种| C{变体未来会增吗?}
  C -->|会| M
  C -->|不会, 如外部类型| I[if let 更轻量]
  B -->|是| I
  B -->|否| M
  A -->|循环消费| W[while let]

let-else:早期返回的优雅写法

Rust 1.65 引入的 let-else 专为「匹配失败就提前返回」设计,是写「前置校验」最顺手的工具。

基本语法与 diverge 要求

语法是 let PATTERN = EXPR else { ... };:匹配成功时,绑定的变量在后续作用域可用;匹配失败时,执行 else 块——而 else必须 diverge(不正常返回):

1
2
3
4
5
6
7
8
9
10
// 提取首个单词,缺失则返回 0
fn first_word_len(text: &str) -> usize {
let Some(first) = text.split_whitespace()
.next() else {
// else 块必须 return/break/panic 等
return 0;
};
// 匹配成功后 first 在此作用域可用
first.len()
}

else 块里必须以 returnbreakcontinuepanic!todo!()unreachable!() 或无限循环等「diverging 表达式」收尾。否则编译失败:

1
2
3
4
5
6
7
8
// 编译失败:else 块未 diverge
// error[E0005]: ... but ... requires a diverging block
fn bad(x: Option<i32>) -> i32 {
let Some(v) = x else {
0 // 普通 i32,不是 diverging
};
v
}

对比 Go 的 if err != nil

let-else 在心智上最接近 Go 的「错误即提前返回」惯用法:

1
2
3
4
5
6
7
8
9
// Go:err != nil 就 return
func port(config map[string]string) int {
raw, ok := config["port"]
if !ok {
return 80 // 缺失就返回默认
}
// raw 在此可用
return atoi(raw)
}
1
2
3
4
5
6
7
8
9
// Rust:let-else 做同样的事,但类型安全
fn port(config: &str) -> u16 {
let raw = config.trim();
let Some(num) = raw.parse::<u16>().ok() else {
return 80; // 解析失败就返回默认
};
// num: u16 在此可用,类型已确定
num
}

🔄 对比:Go 的 if err != nil { return err } 是约定俗成的模式,但编译器不强制——你完全可以忘了 return,让控制流「掉下去」。Rust 的 let-else 由编译器保证:不匹配时你必须 diverge,不会意外 fall through 继续执行后续代码。这是「类型系统替你守住不变式」的又一例。

与 if let + else 的对比

不用 let-else 时,「匹配失败就返回」要用 if let 加一层缩进:

1
2
3
4
5
6
7
8
9
// 旧写法:if let + else,多一层缩进
fn first_word_len_old(text: &str) -> usize {
if let Some(first) = text.split_whitespace().next() {
first.len()
// 后续逻辑都缩进在 if 块里
} else {
0
}
}

当校验有多步时,if let 嵌套会让代码不断右移,形成「箭头形」缩进:

1
2
3
4
5
6
7
8
9
10
11
// 多重校验:箭头形缩进,难以阅读
fn parse_old(s: &str) -> Option<i32> {
if let Some(stripped) = s.strip_prefix("n=") {
if let Ok(n) = stripped.parse() {
if n > 0 {
return Some(n);
}
}
}
None
}

let-else 把每一步校验拍平成「失败就退出」,主体逻辑始终在顶层缩进:

1
2
3
4
5
6
7
8
9
10
11
12
13
// let-else:扁平化,主体逻辑不缩进
fn parse(s: &str) -> Option<i32> {
let Some(stripped) = s.strip_prefix("n=") else {
return None;
};
let Ok(n) = stripped.parse() else {
return None;
};
if n <= 0 {
return None;
}
Some(n) // 主体逻辑在顶层
}

💡 提示let-else 的核心价值是「让 happy path 保持在最小缩进层」。每一步前置校验失败就提前退出,主流程像一条直线。这种写法在解析器、命令行参数处理、HTTP 请求校验等「层层把关」场景下尤其清爽。

模式的完整能力总览

Rust 的「模式」是一套完整的微型语言。下表先给出全貌,后续各节逐一展开:

模式类别示例说明
字面量1true"hi"按值相等匹配
变量绑定x匹配任意值并绑定到 x
通配符_匹配任意值,不绑定
或模式1 | 2 | 3匹配任一子模式
范围1..=10'a'..='z'闭区间匹配
结构体解构Point { x, y }按字段解构
元组解构(a, b, c)按位置解构
枚举解构Some(x)Ok(v)按变体解构
数组/切片[a, b, ..]按元素解构
引用&x&(a, b)解引用匹配
@ 绑定n @ 0..=10测试范围同时绑定
忽略剩余..忽略其余字段/元素
ref/ref mutref x显式借用绑定

字面量、变量绑定与通配符

字面量模式按值相等匹配,支持整数、浮点、布尔、字符、字符串:

1
2
3
4
5
6
7
let grade = 'B';
match grade {
'A' => println!("优秀"),
'B' => println!("良好"),
'C' => println!("及格"),
_ => println!("其他"),
}

注意浮点字面量匹配因精度问题不推荐用于 match——浮点相等比较本身不可靠。

变量绑定模式匹配任意值并把值绑定到该变量名。它「吞噬一切」,所以必须放在最后,否则后面的分支不可达:

1
2
3
4
5
6
let x = 5;
match x {
// 变量 other 会匹配任何值
other => println!("值是 {}", other),
// 5 => ..., // 不可达!警告
}

变量绑定和字面量同名时会引发「遮蔽」陷阱:模式里的 x 是新绑定,而非外部变量。要引用外部变量做相等比较,必须用守卫或限定路径:

1
2
3
4
5
6
7
8
9
10
11
12
let target = 5;
let n = 5;
match n {
// target 这里是「新绑定」,不是外部 target!
// 它会匹配任何值并绑定为 target
target => println!("绑定为 {}", target),
}
// 要比较相等,用守卫:
match n {
x if x == target => println!("命中目标"),
_ => println!("未命中"),
}

⚠️ 注意:这是模式匹配里最经典的陷阱之一——模式中的标识符默认是「新绑定」而非「比较外部值」。Rust 1.x 时代曾有 @ 绑定的相关讨论。要按外部变量做相等匹配,用守卫 x if x == target,或把外部值用 const/路径限定(如 Enum::Variant 这种路径模式才按相等匹配)。

通配符 _ 是「不绑定任何变量的变量模式」。它匹配任意值并丢弃,常作默认分支。_ 与变量绑定的区别仅在于:_ 不发生 move(即使匹配的是非 Copy 类型),因为编译器知道你不用它:

1
2
3
4
5
6
7
let s = Some(String::from("hi"));
match s {
Some(_) => println!("有值,但不要内部"),
// _ 不 move 出 String,s 仍可用
None => {}
}
println!("{:?}", s); // 仍可用

若写成 Some(x),则 x 会 move 走 Strings 之后就不可用了。

或模式与范围模式

或模式| 把多个模式合并为一个,匹配其中任一即可。| 优先级低于 ..=,且各子模式绑定的变量必须「同型同数量」:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let n = 3;
match n {
1 | 3 | 5 | 7 | 9 => println!("奇数个位"),
2 | 4 | 6 | 8 => println!("偶数个位"),
_ => println!("其他"),
}

// 或模式 + 解构,两侧绑定须一致
enum Op {
Add(i32, i32),
Sub(i32, i32),
Mul(i32, i32),
}
let o = Op::Add(2, 3);
match o {
// 两个变体都解构出 (a, b)
Op::Add(a, b) | Op::Sub(a, b) => {
println!("二元运算: {} {}", a, b);
}
Op::Mul(a, b) => println!("乘: {} {}", a, b),
}

或模式中各分支绑定的变量类型必须相同、数量必须相同,否则编译失败。| 还可与 @ 范围组合:n @ (1..=5 | 10..=15)

范围模式..= 表示闭区间,支持整数与字符(char)。开区间 .. 自 Rust 1.80 起在模式中可用(之前只能 ..=):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let c = 'm';
match c {
'a'..='z' => println!("小写字母"),
'A'..='Z' => println!("大写字母"),
'0'..='9' => println!("数字"),
_ => println!("其他"),
}

let age: u32 = 25;
match age {
n @ 0..=12 => println!("儿童 {}", n),
n @ 13..=17 => println!("少年 {}", n),
n @ 18..=64 => println!("成年 {}", n),
n @ 65.. => println!("老年 {}", n), // 开区间上界
// 注意:u32 还有取值,编译器仍可能要求兜底
}

💡 提示:范围模式对整数仍无法穷尽(u32 有 40 亿个值),所以即便你列了 0..=150,还得补 _ 或确保覆盖全部取值。范围模式主要用于 char 与有界的判别值,不要指望它替代 match 的穷尽性。

解构详解

解构是模式匹配最强大的能力之一:把组合类型「拆开」并同时绑定内部数据。Rust 几乎所有复合类型都能解构——结构体、元组、枚举、数组、切片、引用。

结构体解构

结构体按字段名解构,未列出的字段用 .. 忽略:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Point { x: i32, y: i32 }

let p = Point { x: 3, y: 7 };
match p {
Point { x, y: 0 } => println!("在 x 轴: {}", x),
Point { x: 0, y } => println!("在 y 轴: {}", y),
Point { x, y } => println!("({}, {})", x, y),
}

// 只关心部分字段,用 .. 忽略其余
match p {
Point { x, .. } => println!("x = {}", x),
}

字段模式 y: 0 表示「y 字段必须等于 0 才匹配,且不绑定」;y 简写等价于 y: y(绑定到同名变量)。.. 忽略所有未列出的字段,一个模式里只能出现一次 ..

字段名还可做更细的子模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Line { start: Point, end: Point }
let l = Line {
start: Point { x: 0, y: 0 },
end: Point { x: 5, y: 5 },
};
match l {
// 解构嵌套结构体
Line { start: Point { x: 0, y: 0 }, end } => {
println!("从原点出发到 {:?}", end);
}
Line { start, end } => {
println!("{:?} -> {:?}", start, end);
}
}

元组解构

元组按位置解构,_ 跳过单个元素,.. 跳过中间若干元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
let pair = (1, "two", 3.0);
match pair {
(1, s, _) => println!("首元素 1,第二个 {}", s),
(a, b, c) => println!("{} {} {}", a, b, c),
}

// .. 跳过中间元素
let tuple = (1, 2, 3, 4, 5);
match tuple {
(first, .., last) => {
println!("首 {}, 尾 {}", first, last);
}
}

let 语句里解构元组是最常见的不可反驳模式:

1
2
let (a, b, c) = (1, 2, 3); // 不可反驳,必定成功
let (x, rest) = (1, 2); // 元组数量必须匹配

枚举变体解构(含嵌套)

枚举解构是 match 的主战场。解构时必须写出完整路径 Enum::Variant,并按变体定义的数据形状绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
enum Message {
Quit,
Move { x: i32, y: i32 }, // 结构体变体
Write(String), // 元组变体
ChangeColor(i32, i32, i32), // 元组变体
}

fn process(msg: Message) {
match msg {
Message::Quit => println!("退出"),
Message::Move { x, y } => {
println!("移动到 ({}, {})", x, y);
}
Message::Write(text) => {
println!("写入: {}", text);
}
Message::ChangeColor(r, g, b) => {
println!("颜色 #{:02x}{:02x}{:02x}", r, g, b);
}
}
}

枚举变体有三种数据形状,对应三种解构语法:

变体形状定义解构语法
单元变体QuitMessage::Quit
元组变体Write(String)Message::Write(s)
结构体变体Move { x, y }Message::Move { x, y }

嵌套枚举同样可层层解构。一个表达式语法树是典型例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum Expr {
Num(f64),
Add(Box<Expr>, Box<Expr>),
Neg(Box<Expr>),
}

fn eval(e: &Expr) -> f64 {
match e {
Expr::Num(n) => *n,
// 嵌套解构两层 Box<Expr>
Expr::Add(a, b) => eval(a) + eval(b),
Expr::Neg(inner) => -eval(inner),
}
}

let e = Expr::Add(
Box::new(Expr::Num(2.0)),
Box::new(Expr::Neg(Box::new(Expr::Num(5.0)))),
);
assert_eq!(eval(&e), -3.0);

更多枚举变体的定义与内存布局见《枚举》

数组与切片解构

定长数组按元素解构,长度必须精确匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let arr = [1, 2, 3];
match arr {
[0, _, _] => println!("首元素 0"),
[a, b, c] => println!("{} {} {}", a, b, c),
}

// 用 .. 匹配中间任意长度(仅切片)
let slice: &[i32] = &[1, 2, 3, 4, 5];
match slice {
[first] => println!("单元素 {}", first),
[first, ..] => println!("至少一个,首 {}", first),
[.., last] => println!("尾 {}", last),
[a, .., z] => println!("首 {} 尾 {}", a, z),
[] => println!("空切片"),
}

数组解构的长度是类型的一部分[i32; 3][i32; 4] 是不同类型,模式必须写对长度。切片解构则更灵活,.. 可匹配任意长度,常用于「取首/尾元素」。

💡 提示[first, ..]slice.first() 等价但更直接。在解析固定头部 + 变长尾部的二进制协议时,[cmd, payload @ ..] 这种写法极为顺手——@ 把可变切片绑定到 payload

引用解构:& 与 &mut

模式里的 & 解引用匹配,&mut 匹配可变引用:

1
2
3
4
5
6
7
8
9
10
11
let r: &(i32, i32) = &(1, 2);
match r {
&(a, b) => println!("{} {}", a, b),
// a, b 是 i32(已解引用),不是 &i32
}

let mut v = 10;
let rmut: &mut i32 = &mut v;
match rmut {
&mut n => println!("{}", n),
}

&(a, b)&(i32, i32) 解构出 a: i32b: i32。这是「显式」写法。实际中更常用「匹配人体工程学」自动处理引用,见下一节。

多层嵌套解构

模式可任意深度嵌套,把复杂结构一次性拆到原子:

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
struct Node {
name: String,
left: Option<Box<Node>>,
right: Option<Box<Node>>,
}

let root = Node {
name: String::from("root"),
left: Some(Box::new(Node {
name: String::from("a"),
left: None,
right: None,
})),
right: Some(Box::new(Node {
name: String::from("b"),
left: None,
right: None,
})),
};

match &root {
// 多层嵌套:结构体 + Option + Box + 结构体
Node {
name,
left: Some(first),
..
} => {
println!("根 {} 的左子 {}", name, first.name);
}
Node {
name,
left: None,
right: None,
} => {
println!("叶子节点 {}", name);
}
_ => println!("其他"),
}

嵌套解构让「深挖特定形状」的代码极为紧凑,但可读性需权衡——过深的嵌套会让模式难以一眼看懂,此时拆成多个 match 或用辅助函数更清晰。

绑定模式与 match ergonomics

模式匹配与引用的交互是 Rust 独有的难点。

ref / ref mut 显式绑定

默认情况下,模式中的变量按「值」绑定——会 move 出非 Copy 数据。若你只持有引用却想「借用」内部数据,需用 ref/ref mut 显式声明:

1
2
3
4
5
6
7
8
9
10
11
12
let name = String::from("Alice");
let opt = Some(name);

// opt 持有 String,直接 match 会 move
match opt {
Some(s) => {
// s: String,move 走,opt 失效
println!("{}", s);
}
None => {}
}
// opt 已被 move,不可再用

若想保留 opt 的所有权,用 ref 借用:

1
2
3
4
5
6
7
8
9
let opt = Some(String::from("Alice"));
match opt {
Some(ref s) => {
// s: &String,借用,opt 仍有效
println!("{}", s);
}
None => {}
}
println!("{:?}", opt); // 仍可用

ref s 表示「把匹配到的值以 & 形式绑定到 s」。ref mut 是可变借用版本,要求被匹配值本身是 mut

1
2
3
4
5
6
7
8
let mut opt = Some(String::from("hi"));
match opt {
Some(ref mut s) => {
s.push_str("!"); // 通过 &mut 修改内部
}
None => {}
}
println!("{:?}", opt); // Some("hi!")

⚠️ 注意ref 写在模式里(绑定处),& 写在类型/值处。Some(ref s)Some(&s) 含义完全不同:前者「借用匹配到的值」,后者「匹配一个引用」。混淆它们是新手常犯错误。

匹配人体工程学(match ergonomics)

Rust 2018 引入「匹配人体工程学」(match ergonomics,RFC 2008),让你在匹配引用时省略 &/ref,编译器自动推断「默认绑定模式」。对比两种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
let opt: &Option<i32> = &Some(5);

// 老写法(Rust 2015):显式 &
match opt {
&Some(x) => println!("{}", x), // x: i32
&None => println!("none"),
}

// 新写法(match ergonomics):省略 &
match opt {
Some(x) => println!("{}", x), // x: i32(自动解引用)
None => println!("none"),
}

当被匹配值是引用时,编译器进入「默认绑定模式」:模式中的变量默认以引用形式绑定,自动调整成合适的 &/&mut/ref。规则大致是:

  • 匹配 &T 时,模式里的字段默认绑定为 &T 的字段的引用;
  • 匹配 &mut T 时,按需绑定为 &mut 或共享引用。
1
2
3
4
5
6
7
8
let p: &Point = &Point { x: 1, y: 2 };
match p {
// p 是 &Point,x/y 自动绑定为 &i32
Point { x, y } => {
let xr: &i32 = x; // x 确实是 &i32
println!("{} {}", xr, y);
}
}

这套机制让 90% 的场景无需手写 ref/&,代码更干净。

默认绑定模式演进

「默认绑定模式」会随匹配值的引用性「传染」到子模式。理解它的关键是:每一步匹配都会更新当前的「绑定模式」(默认 move,遇到 &ref,遇到 &mutref mut):

flowchart TD
  S[被匹配值类型] --> V{是引用?}
  V -->|否, 拥有所有权| M[默认 move 绑定]
  V -->|是 &T| R[默认 ref 绑定]
  V -->|是 &mut T| RM[默认 ref mut 绑定]
  M --> D[子模式按 move 解构]
  R --> DR[子模式按 ref 解构]
  RM --> DRM[子模式按 ref mut 解构]
  D --> B[变量类型 = 字段类型]
  DR --> B2[变量类型 = &字段类型]
  DRM --> B3[变量类型 = &mut 字段类型]

实践中你不必手动追踪这套状态——写出模式让编译器跑一遍,错误信息会指引你。但理解原理能帮你读懂「为什么这里 x&i32 而不是 i32」。

🔬 进阶:match ergonomics 引入前,匹配引用必须处处写 &/ref,代码冗长易错。RFC 2008 之后,ref 几乎只出现在「匹配拥有所有权的值却想借用」的少数场景。但当你显式写 ref 时,它会覆盖默认绑定模式,这在混合 moveborrow 的复杂 match 里仍有用武之地。

@ 绑定:测试的同时保留值

@ 绑定让你在用范围或模式「测试」一个值的同时,把整个值绑定到一个变量。没有 @,测试通过后你只能拿到解构出的子部分。

范围 + 绑定

最常见的用法是范围测试 + 绑定:

1
2
3
4
5
6
7
let age: i32 = 25;
match age {
n @ 0..=12 => println!("儿童: {}", n),
n @ 13..=17 => println!("少年: {}", n),
n @ 18..=64 => println!("成年: {}", n),
n @ _ => println!("老年: {}", n), // 等价于 n
}

若不用 @0..=12 匹配后你拿不到具体的 n 值(除非用 if 守卫),只能知道「在范围内」。@ 把「在范围内」与「具体是多少」一次性拿到。

子绑定

@ 还可在解构子结构时绑定中间值。比如匹配 Option 同时保留整个 Option 和内部值:

1
2
3
4
5
6
7
8
let opt: Option<i32> = Some(7);
match opt {
// o 绑定整个 Some(7),x 绑定内部 7
o @ Some(x) if x > 5 => {
println!("大值 {:?}, 内部 {}", o, x);
}
_ => {}
}

这在错误处理时很有用——你想既记录「整个 Result」,又取出内部的错误信息。

绑定同时测试值

@ 与或模式、范围组合可表达复杂条件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let n = 15;
match n {
// 同时匹配多个范围,绑定到 n
n @ (1..=5 | 10..=15) => {
println!("命中区间: {}", n);
}
_ => println!("未命中"),
}

// 结构体字段 + @ 绑定
struct Config { port: u16 }
let cfg = Config { port: 8080 };
match cfg {
// p 绑定 port 的值,同时要求在范围
Config { port: p @ 0..=1023 } => {
println!("特权端口 {}", p);
}
Config { port: p @ 1024.. } => {
println!("普通端口 {}", p);
}
}

💡 提示@ 的本质是「为子模式命一个名字」。当你既想用模式断言形状,又想保留被断言的值,@ 就是答案。它在解析器(保留 token 同时检查类型)、校验逻辑(保留原值同时检查范围)里尤为常用。

守卫条件进阶

守卫(if 子句)扩展了模式的表达力,但也带来若干限制与陷阱。

附加布尔过滤

守卫是分支的布尔过滤条件,可引用分支内绑定的变量:

1
2
3
4
5
6
let pair = (2, 3);
match pair {
(a, b) if a < b => println!("{} < {}", a, b),
(a, b) if a > b => println!("{} > {}", a, b),
(a, b) => println!("{} == {}", a, b),
}

守卫可访问外部变量,这让模式能依赖运行时上下文:

1
2
3
4
5
6
let threshold = 10;
let n = 15;
match n {
x if x > threshold => println!("超阈值"),
x => println!("未超阈值"),
}

守卫的限制

守卫有几个不那么直观的限制,是陷阱高发区:

1. 绑定仅在分支内有效。守卫里能用分支绑定的变量,但守卫求值发生在「模式匹配成功后、分支体执行前」,绑定的变量生命周期仅限本分支:

1
2
3
4
5
6
7
match Some(5) {
Some(x) if x > 0 => {
// x 在分支体和守卫里都可用
println!("{}", x);
}
_ => {}
}

2. 守卫可能被多次求值。编译器在某些优化下可能对守卫求值不止一次,因此守卫不应有副作用。下例是反面教材:

1
2
3
4
5
6
7
8
9
// 反例:守卫里有副作用
let mut count = 0;
match n {
x if { count += 1; x > 0 } => {
// count 的增量次数不确定
println!("{}", x);
}
_ => {}
}

实际上守卫只接受 bool 表达式,不能用块语句做副作用(块若返回 bool 形式上可行但极不推荐)。规范用法是把副作用放在分支体里。

3. 守卫破坏穷尽性保证。带守卫的分支「可能不命中」,因此编译器对带守卫的 match 更保守——通常仍要求一个不带守卫的兜底分支:

1
2
3
4
5
6
7
8
9
// 无兜底会编译失败:守卫可能都不满足
fn classify(n: i32) -> &'static str {
match n {
x if x > 0 => "正",
x if x < 0 => "负",
// 缺少 x == 0 与兜底
_ => "零", // 必须有
}
}

4. 守卫不能引用未绑定的模式变量| 或模式里的变量在守卫里可见,但若同一分支的 | 两侧绑定不同变量,守卫里用哪个就成问题:

1
2
3
4
5
6
7
// 编译失败:或模式两侧绑定名不同
match (1, 2) {
(a, 2) | (1, b) if a == 1 => {}
// a 仅在左侧绑定,b 仅在右侧
// 守卫引用 a 在右侧不成立
_ => {}
}

⚠️ 注意:守卫是 Rust 模式匹配里最容易踩坑的部分。原则是:守卫只做纯判断,不依赖求值次数、不带副作用、不假设穷尽性。需要复杂判断时,把逻辑挪到分支体里用普通 if 表达,比塞进守卫更清晰。

可反驳 vs 不可反驳模式

模式分两大类:不可反驳的(irrefutable)和可反驳的(refutable)。这一区分决定了模式能用在哪些位置。

定义

  • 不可反驳模式:对任何输入都匹配成功。如 x(变量绑定)、_(a, b)(元组解构,前提是类型确实是二元组)、Point { x, y }(结构体解构,前提类型是 Point)。
  • 可反驳模式:对某些输入可能匹配失败。如 Some(x)(可能是 None)、Ok(v)(可能是 Err)、1(值可能不是 1)、&(_, _)
1
2
let (a, b) = (1, 2);  // 不可反驳:必成功
if let Some(x) = opt { } // 可反驳:可能 None

哪些位置接受哪种

不同位置对模式类型有不同要求:

位置接受的模式示例
let 语句仅不可反驳let (a, b) = t;
函数参数仅不可反驳fn f((a, b): (i32, i32))
for 循环仅不可反驳for (k, v) in map
match 分支可反驳(至少一个能匹配)match opt { Some(x) => .. }
if let可反驳if let Some(x) = opt
while let可反驳while let Some(x) = s.pop()
let-else可反驳let Some(x) = opt else {..}
闭包参数仅不可反驳|(a, b)| ..

混用会编译报错

在只接受不可反驳模式的位置写可反驳模式,编译器直接拒绝:

1
2
3
// 编译失败:if let 模式用在 let 位置
// error[E0005]: refutable pattern in local binding
let Some(x) = opt; // Some(x) 是可反驳的

编译器会建议改用 if letlet else

1
2
3
4
5
6
error[E0005]: cannot find unreachable pattern
--> |
| let Some(x) = opt;
| ^^^^^^^ pattern `Some(x)` is refutable
help: ...
| let Some(x) = opt else { ... };

反过来,if let 必须用可反驳模式——写不可反驳模式会得到「无意义」警告:

1
2
3
4
5
// 警告:不可反驳模式用在 if let
// warning: irrefutable `if let` pattern
if let x = 5 {
println!("{}", x); // 永远成立,if let 多余
}

💡 提示:理解可反驳/不可反驳的分界,能帮你快速定位「为什么这里编译器让我改用 if let」。规则很简单:let/函数参数/for 只接受「必成功」的模式,因为它们没有「失败时怎么办」的语法;match/if let/while let/let-else 都有失败处理路径,故接受可反驳模式。

模式的限制与陷阱

模式匹配与所有权、借用深度耦合,催生了一批独特的陷阱。

移动语义在 match 中的影响

match 一个拥有所有权的枚举时,命中的分支会按模式绑定「move」出内部数据。这会消耗被匹配的值:

1
2
3
4
5
6
7
8
9
let msg = Message::Write(String::from("hi"));
match msg {
Message::Write(text) => {
// text: String,被 move 出来
println!("{}", text);
}
_ => {}
}
// msg 已被 move,不可再用

若多个分支都想 move 不同的内部数据,看起来像「同一个值被 move 多次」,实则安全——因为只有命中分支才真正执行:

1
2
3
4
5
6
7
match msg {
Message::Write(t) => consume(t), // 命中才 move
Message::Move { x, y } => {
consume2(x, y); // 命中才 move
}
_ => {}
}

但若分支只用 _ 忽略,则不 move,原值仍可用(前文通配符已述)。问题出在「想同时 move 部分字段、保留其他」时——枚举是整体,无法「只 move 一个字段」。这时要么 match 借用,要么用 take 之类的方法换出内部值。

as_ref 惯用法

Option/Result 持有非 Copy 数据(如 String),直接 match 会消耗它。若你只想「看一下」内部值,用 as_ref 先借用:

1
2
3
4
5
6
7
8
9
10
11
let opt = Some(String::from("hello"));

// 直接 match 会 move 出 String
// match opt { Some(s) => .. } 之后 opt 失效

// as_ref 借用:Option<String> -> Option<&String>
match opt.as_ref() {
Some(s) => println!("len = {}", s.len()), // s: &String
None => {}
}
println!("{:?}", opt); // opt 仍可用

as_refOption<T> 转成 Option<&T>as_mut 转成 Option<&mut T>as_deref 进一步转成 Option<&<T as Deref>::Target>(如 Option<&String>Option<&str>)。这套方法是与所有权协作的核心惯用法,呼应《所有权、借用与生命周期》

1
2
3
4
5
6
7
8
9
let mut opt = Some(String::from("hi"));
// as_mut:可变借用,原地修改
if let Some(s) = opt.as_mut() {
s.push_str("!");
}
// as_deref:&String -> &str
if let Some(s) = opt.as_deref() {
println!("{}", s); // s: &str
}

守卫副作用与多次求值

前文提到守卫可能被多次求值。这不仅是「别写副作用」的告诫,还有更微妙的后果:带守卫的分支不能依赖「至多求值一次」。例如用守卫做带状态的过滤,结果可能不可预期。规范做法是把判断移入分支体,用普通 if 表达。

绑定模式与 move 的冲突

当一个模式既想绑定整个值(@)又想 move 子部分,会与所有权规则冲突:

1
2
3
4
5
6
7
8
9
10
let opt = Some(String::from("hi"));
match opt {
// 想同时保留整个 Some 和 move 出 String
o @ Some(s) => {
// o: Option<String>, s: String
// 但 move 出 s 后 o 失去内部数据,矛盾
println!("{:?} {}", o, s);
}
None => {}
}

这种「整体 + 部分」的 move 在拥有所有权时无法两全。解法是 match 借用:match &opt { o @ Some(s) => .. },此时 o: &Option<String>s: &String,皆大欢喜。这也是 as_ref 惯用法的另一动机。

⚠️ 注意:遇到「编译器抱怨值被 move 后仍被使用」(use of partially moved valueuse of moved value),多半是 match 消耗了所有权。第一反应应是:能否 match &valmatch val.as_ref(),用借用代替 move?这能解决大半问题。

实战示例

解析器:键值对与命令行参数

解析 key=value 形式的配置行,let-else + 模式匹配让代码扁平而清晰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn parse_kv(line: &str) -> Option<(&str, &str)> {
let line = line.trim();
// 用模式 + let-else 逐步校验
let Some((key, value)) = line.split_once('=') else {
return None; // 无等号
};
let key = key.trim();
let value = value.trim();
if key.is_empty() {
return None;
}
Some((key, value))
}

fn main() {
for line in ["port=8080", " host = localhost", "bad"] {
match parse_kv(line) {
Some((k, v)) => println!("{} => {}", k, v),
None => println!("无法解析: {:?}", line),
}
}
}

命令行参数分发用 match 对子命令穷尽匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
enum Cmd {
Help,
Version,
Add { title: String },
Done { id: u32 },
Unknown(String),
}

fn parse_cmd(args: &[&str]) -> Cmd {
match args {
// 切片模式:按形状分发
[] | [_] | [_, "--help"] => Cmd::Help,
[_, "--version"] => Cmd::Version,
[_, "add", title] => {
Cmd::Add { title: title.to_string() }
}
[_, "done", id] => match id.parse() {
Ok(n) => Cmd::Done { id: n },
Err(_) => Cmd::Unknown(id.to_string()),
},
[_, unknown, ..] => Cmd::Unknown(unknown.to_string()),
}
}

切片解构 [_, "add", title] 极其直观——「第一个是程序名,第二个是 add,第三个是标题」。这种写法比 if args[1] == "add" 既安全又表意。

状态机分发

用枚举建模状态机,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
enum State {
Idle,
Connecting { addr: String },
Connected { addr: String },
Error { code: u16 },
}

enum Event {
Connect(String),
Connected,
Disconnect,
Fail(u16),
}

fn transition(s: State, e: Event) -> State {
match (s, e) {
// 元组解构:同时匹配状态与事件
(State::Idle, Event::Connect(addr)) => {
State::Connecting { addr }
}
// 连接中收到确认:提升为已连接
(State::Connecting { addr }, Event::Connected) => {
State::Connected { addr }
}
// 连接失败:丢弃 addr,记录错误码
(State::Connecting { .. }, Event::Fail(c)) => {
State::Error { code: c }
}
(State::Connected { .. }, Event::Disconnect) => State::Idle,
// 其余组合:保持原状
(s, _) => s,
}
}

新增状态或事件时,编译器会在每个 match 提醒你补全转移,杜绝「漏处理某条边」的状态机 bug。

AST 遍历

表达式 AST 的求值与化简,是嵌套解构的经典舞台:

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
#[derive(Clone)]
enum Expr {
Num(f64),
Var(&'static str),
Neg(Box<Expr>),
Add(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
}

fn eval(e: &Expr, env: &dyn Fn(&str) -> f64) -> f64 {
match e {
Expr::Num(n) => *n,
Expr::Var(name) => env(name),
Expr::Neg(inner) => -eval(inner, env),
Expr::Add(a, b) => eval(a, env) + eval(b, env),
Expr::Mul(a, b) => eval(a, env) * eval(b, env),
}
}

// 常量折叠:化简 AST
fn fold(e: &Expr) -> Expr {
match e {
Expr::Neg(inner) => {
let inner = fold(inner);
match inner {
// 折叠双重取负:--x = x
f @ Expr::Neg(_) => f,
other => Expr::Neg(Box::new(other)),
}
}
Expr::Add(a, b) => {
let a = fold(a);
let b = fold(b);
match (a, b) {
// 0 + x = x
(Expr::Num(0.0), rest) => rest,
(rest, Expr::Num(0.0)) => rest,
(a, b) => Expr::Add(Box::new(a), Box::new(b)),
}
}
other => other.clone(),
}
}

这里 match (&a, &b) 同时解构两个表达式的形状,Expr::Num(0.0) 字面量模式精确匹配零。模式匹配让「形状驱动的化简」表达得几乎像数学公式。

配置解构

把嵌套配置一次性解构成扁平字段,减少中间变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct DbConfig {
host: String,
port: u16,
user: String,
}
struct AppConfig {
db: DbConfig,
debug: bool,
}

fn connect(cfg: &AppConfig) {
// 一次性解构两层结构体
let AppConfig {
db: DbConfig { host, port, .. },
debug,
} = cfg;
let level = if *debug { "debug" } else { "info" };
println!("[{}] 连接 {}:{} ", level, host, port);
}

let 解构 + .. 让你「按需取字段」,不必 cfg.db.host 一长串。配合 match ergonomics,cfg 是引用时 host/port 自动是 &String/&u16

最佳实践与速查表

何时用哪种构造

  • 全部分支都要处理、变体可能增加 → match(享穷尽性);
  • 只关心一种变体、变体稳定不变 → if let(轻量);
  • 逐项消费迭代器/栈 → while let
  • 前置校验、失败即返回 → let-else(happy path 不缩进)。

模式编写要点

  • 自家枚举显式列变体,慎用 _(会吞掉新增变体);
  • 默认分支用 unreachable!()/panic!() 带说明,优于静默 _ => {}
  • 解构复合类型用 .. 忽略不关心的字段;
  • 匹配引用优先靠 match ergonomics,必要时 as_ref/as_mut 借用;
  • 既测试范围又保留值用 @
  • 守卫保持纯判断,无副作用、不假设穷尽。

所有权协作要点

  • match 拥有所有权的值会 move 内部数据,需保留时用 match &val
  • Option/Result 持非 Copy 数据时,as_ref/as_mut/as_deref 是惯用借用;
  • _ 不 move,变量 会 move——忽略值时用 _

模式能力速查表

模式语法典型用途
字面量1'a'"x"按值相等
变量绑定x绑定任意值
通配符_忽略,不 move
或模式a | b | c多值合并
范围0..=9'a'..='z'区间匹配
结构体S { x, y }字段解构
元组(a, b, ..)位置解构
枚举E::V(x)变体解构
切片[a, .., z]序列首尾
引用&x&mut x解引用
@ 绑定n @ 0..=9测试 + 绑定
ref/ref mutref x显式借用
忽略剩余..跳过其余字段

可反驳性速查

位置接受可反驳?
let、函数参数、for、闭包参数否(仅不可反驳)
matchif letwhile letlet-else

🔬 进阶:模式匹配是 Rust 类型系统的「解构面」——它和 trait、泛型共同构成了 Rust 的多态能力。trait 提供运行时/编译时多态(见《类型系统》),而模式匹配提供「按形状分支」的多态。二者常配合使用:一个返回枚举的 trait 方法,调用方用 match 解构结果。掌握模式匹配,你就握住了 Rust 表达力的另一半。

下一章《类型系统:泛型、trait 与多态》介绍 Rust 如何用泛型与 trait 在编译期实现多态,以及它们与模式匹配如何协作。