# 模式匹配

Source: https://codewiki.com/zh/rust/pattern-matching/

> - **what**: 模式匹配用一个模式检查值的形状与内容，还能在同一步中取出内部数据；`match` 会把这套机制用于穷尽的分支选择。
> - **trap**: `_` 会让代码在枚举新增变体后继续编译，却可能悄悄走错分支；按值解构还可能移动非 `Copy` 字段，使原值整体失效。
> - **fix**: 对封闭枚举优先列出所有变体，先决定要借用还是取得所有权，再选择 `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` 分支仍负责其余大小。

<!-- quick -->

```rust
// file: route.rs
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}");
    }
}
```

```text
health: 200
invalid id: 400
read: 200
too large: 413
```


<!-- /quick -->

如果新增 `Request::Delete`，此函数会因匹配不穷尽而停止编译。若用 `_` 取代最后两个显式变体分支，这个提醒便会消失，这正是通配分支有时会削弱演进安全性的原因。

`id: 0` 是字段子模式，不会绑定 `id`。下一个分支用 `..` 表示该变体的其余字段对当前决定无关，也不会承诺以后新增字段时读取它们。

### 用切片模式解析命令

`split_first()` 先把空输入从主路径排除，`let-else` 让后续代码直接使用 `verb` 与 `args`。`match` 再根据动词和参数切片的长度同时检查命令形状。

```rust
// file: commands.rs
#![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));
    }
}
```

```text
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` 字段。

```rust
// file: binding_modes.rs
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));
}
```

```text
retry 2 for Mina
owner: Mina
```

`attempts @ 1..=3` 既测试闭区间，也保留实际重试次数。因为匹配对象是共享引用，`owner` 不会移走字符串；`inspect()` 返回后，该借用结束。

`take_owner()` 消费整个 `Job`，所以移动 `owner` 符合函数契约。若调用方之后仍要使用 `job`，正确做法通常是让函数接收 `&Job` 或 `&str`，而不是为了让旧签名编译就克隆整个值。

### 在循环中只处理连续成功值

`while let` 很适合消费返回 `Option` 或 `Result` 的状态型操作。这个例子逐个弹出字符串，`let-else` 跳过解析失败项，再用范围和守卫分类有效数字。

```rust
// file: readings.rs
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());
}
```

```text
odd: 7
even: 42
skip: bad
outside: 101
empty: true
```

向量采用后进先出顺序，所以输出从 `"7"` 开始。解析失败通过 `continue` 满足 `let-else` 必须离开当前路径的要求，而不合法输入是否应跳过，应由调用方契约决定。

第一个分支的守卫只筛选偶数；第二个范围分支仍需接住区间内的奇数。把第二个分支删掉不会让第一个守卫代表整个 `0..=100`，编译器也不会把任意 `%` 条件纳入覆盖证明。

## 陷阱

> **陷阱:** **用 `_` 掩盖枚举演进。** 生成代码常用 `_ => unreachable!()` 快速消除不穷尽错误，但新增合法变体后会从编译期提醒变成运行时 panic。
>
> **修复：** 对本 crate 内的封闭枚举明确列出每个变体。只有未知值确实共享相同语义，或匹配的是开放输入空间时才用 `_`，并让兜底行为安全可观察。

> **陷阱:** **把裸标识符当成外层值。** `match code { expected => ... }` 中的 `expected` 通常创建一个匹配任意值的新变量，还会遮蔽外层同名变量，后续分支因此不可达。
>
> **修复：** 比较运行时值时使用守卫，如 `value if value == expected`。固定值应使用可解析为路径的 `const` 或枚举变体，并检查 `unreachable_patterns` 警告。

> **陷阱:** **以为守卫完成了穷尽覆盖。** `Some(value) if value > 0` 没有覆盖零与负数，而且编译器不会证明任意守卫的逻辑补集。
>
> **修复：** 在守卫分支后保留覆盖其余结构的分支，例如 `Some(value)`，再处理 `None`。为边界值和守卫为 `false` 的路径写测试。

> **陷阱:** **解构时意外移动字段。** 对拥有型结构按值绑定 `String`、`Vec` 或其他非 `Copy` 字段会移动它；代码生成器随后常加入昂贵或语义错误的 `.clone()` 来修补 E0382。
>
> **修复：** 只读时匹配 `&value`，修改时匹配 `&mut value`，确实转移所有权时才匹配 `value`。若发生部分移动，分别使用剩余字段，或重构为完整消费，而不要再使用整体。

> **陷阱:** **用 `if let` 丢掉有意义的失败分支。** `if let Ok(value) = result` 在没有 `else` 时静默忽略错误，可能把导入或配置失败伪装成成功。
>
> **修复：** 当两个及以上分支影响结果时使用 `match` 或传播错误。`if let` 只用于失败路径确实是“什么也不做”的情况，并在测试中明确这一行为。

<!-- deep -->

## 可反驳性不是运行时猜测

编译器从类型和模式形式判断一个模式是否可能失败。变量绑定、只有变量的元组解构以及字段齐全的单一结构体模式通常不可反驳；枚举变体、字面值、范围和限定长度的切片模式通常可反驳。

同一段模式换到不同语法位置，要求也随之变化。普通 `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`。

<!-- /deep -->

[检查点: rust/pattern-matching](https://codewiki.com/zh/rust/pattern-matching/#checkpoint)

## 延伸阅读

- [Rust 1.98 Reference：模式](https://doc.rust-lang.org/1.98.0/reference/patterns.html)
- [Rust 1.98 Reference：`match` 表达式](https://doc.rust-lang.org/1.98.0/reference/expressions/match-expr.html)
- [The Rust Programming Language：模式与匹配](https://doc.rust-lang.org/1.98.0/book/ch19-00-patterns.html)
- [The Rust Programming Language：模式语法](https://doc.rust-lang.org/1.98.0/book/ch19-03-pattern-syntax.html)
- [Rust 1.98 标准库：`matches!`](https://doc.rust-lang.org/1.98.0/std/macro.matches.html)
