Chapter 6:枚举与模式匹配(Enums and Pattern Matching)
对齐官方第 6 章:用携带不同数据的枚举表达互斥状态,以 Option 显式建模缺失值,并掌握 match、捕获模式、if let 与 let...else 的控制流保证。
学习目标
- 能区分 struct 与 enum 的建模职责,用带数据的枚举变体表达“多种形状中恰好一种”
- 能分析
Option<T>如何把值存在或缺失写进类型,并在取出内部值前显式处理Some与None - 能实现穷尽的
match,使用模式绑定和捕获分支,并根据分支数量选择if let或let...else
为什么 enum 要表达互斥状态
struct 表达“一个值同时拥有这些字段”,enum 表达“一个值只能是这些变体中的一个”。例如 IP 地址要么是 IPv4,要么是 IPv6;若把两者都做成可选 struct 字段,就会产生“两个都存在”或“两个都缺失”的无效组合。枚举的 tag 在运行时标识当前变体,静态类型则保证值始终属于已声明集合。
↡由 enum 关键字定义、列出有限个互斥变体的自定义类型;任一实例在某一时刻只属于一个变体。最简单的枚举只区分身份:
enum IpAddrKind {
V4,
V6,
}
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;IpAddrKind::V4 和 IpAddrKind::V6 都是 IpAddrKind。函数接收一个 IpAddrKind 就能接受任一变体,同时编译器知道所有合法状态的封闭集合。
变体可以直接携带不同形状的数据
枚举比“kind 字段加外部 payload”更强之处,是每个变体可以拥有不同数量、不同类型和不同命名方式的数据:
↡直接存放在某个枚举变体中的值;只有该变体的模式匹配成功后,数据才会被绑定和使用。enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));V4 变体像四字段 tuple struct,V6 变体像单字段 tuple struct,但二者不需要额外声明类型,且共同构成 IpAddr。更复杂的消息协议可以在同一 enum 中混合无数据、命名字段和 tuple 数据:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) {
// 根据当前变体执行行为
}
}枚举和结构体一样可以拥有 impl 块。方法接收的是整个 enum;方法内部仍需通过模式匹配区分变体,才能访问某个变体独有的数据。
先预测每个变体会显示哪种 payload 形状,再切换四种 Message 变体,比较构造器、payload 形状和匹配后的行为。关键约束不是“所有变体字段一致”,而是每个变体可以精确声明自己需要的数据,其他状态根本不存在那些字段。
Option 把“可能没有值”纳入类型系统
Rust 标准库用一个通用枚举表达可选值:
enum Option<T> {
None,
Some(T),
}Option<T> 已在 prelude 中,不需要导入;Some 和 None 也可直接使用。编译器常能从 Some(5) 推断 Option<i32>,但单独的 None 没有内部值提供线索,必要时要注解:
let present = Some(5); // Option<i32>
let absent: Option<i32> = None;
let x: i32 = 5;
let y: Option<i32> = Some(5);
// let sum = x + y; // 编译错误:i32 与 Option<i32> 不同Rust 没有让普通引用或普通数值静默携带 null 的设计。Option<T> 与 T 是不同静态类型;在使用内部 T 前,程序必须处理值缺失的路径。这不会消灭业务上的“没有值”,而是消灭忘记检查的隐式路径。
勾选值是否存在,再观察 plus_one 结果。Some(5) 不是 5 本身;模式成功后绑定内部 i32 才能加一,None 则保持 None。
match 同时匹配形状、绑定数据并产生值
↡把一个值依次与多个模式臂比较的控制流表达式;第一个匹配 arm 执行,所有 arm 的结果类型必须兼容。 ↡描述值结构并可绑定其中组成部分的语法形状,例如 Coin::Quarter(state) 或 Some(value)。下面的函数把硬币变体映射为美分值,并在 quarter 分支取出州信息:
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter(state) => {
println!("State quarter from {state:?}!");
25
}
}
}每个 pattern => expression 是一个 arm,arm 之间用逗号分隔。单表达式不需要花括号;多条语句用块,块的尾表达式成为该 arm 的值。Coin::Quarter(state) 先检查 tag,再把关联的 UsState 绑定到局部变量 state。
穷尽性是 match 的核心保证。新增一个 enum 变体后,所有依赖完整分类的 match 都会暴露遗漏;这把“新增状态后哪些逻辑需要更新”转换成编译器可定位的任务。
用 match 处理 Option 的两条路径
官方 plus_one 示例展示了 matching Option<T>:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
assert_eq!(plus_one(Some(5)), Some(6));
assert_eq!(plus_one(None), None);Some(i) 绑定内部值,函数计算后重新包装为 Some;None arm 保留缺失。若删掉 None arm,编译器会报告 non-exhaustive patterns。Option<T> 因而把缺失路径从注释约定提升为必须处理的类型事实。
捕获模式处理剩余值,_ 明确忽略
枚举集合可以逐个列举,但整数等类型无法合理写出每个值。最后一个 arm 常用变量模式捕获此前未匹配的任意值:
↡用变量名匹配此前未覆盖的任意值,并把该值绑定到变量;必须放在更具体模式之后。let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}变量 other 可以匹配任何值,因此若放在最前面,后续 arm 永远不可达。若剩余值不需要使用,用 _:
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}若剩余情况连动作都不需要,用 unit 值 () 作为 arm 表达式。_ => () 明确表示“其余值被考虑过,但有意不做事”,不同于忘记分支。
if let 压缩只关心一个模式的 match
↡只在一个模式匹配时执行代码块的简洁控制流;可带 else 合并所有不匹配情况,但不提供 match 的显式穷尽提醒。如果只想在值是 Some(3) 时执行动作,完整 match 会包含一个只做 () 的 arm:
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
} else {
println!("No maximum configured");
}if let PATTERN = EXPRESSION 在模式成功时绑定变量并进入块;else 等价于 match 的所有其他分支。代价是失去穷尽性检查:当 enum 增加变体时,代码不会自动要求逐项重新判断。选择标准是“是否只关心一个模式”,而不是单纯追求行数最少。
let...else 让失败路径提前离开
↡把一个可反驳模式绑定到后续主路径的语句;不匹配时执行必须发散的 else 块,例如 return、break、continue 或 panic。当函数主路径需要内部值,而失败只应提前返回时,let...else 避免增加缩进:
fn describe(maybe_name: Option<String>) {
let Some(name) = maybe_name else {
println!("missing name");
return;
};
println!("name length = {}", name.len());
}成功匹配后,name 在语句之后的外层作用域可用。else 必须 diverge,不能正常落回后续代码,否则后续路径无法保证 name 已绑定。if let 适合“匹配时做一件事”,let...else 适合“失败提前离开,成功继续主流程”。
在三种模式控制流间切换,并切换输入是否匹配。比较它们的静态保证:match 强制穷尽,if let 合并其余路径,let...else 则要求失败路径终止当前主流程。
三步完成枚举控制流设计
第一步:把合法状态做成变体
识别领域中互斥的状态,为每个状态只携带它实际需要的数据。避免 kind 与 payload 分离后出现矛盾组合。
小结
- enum 表达有限个互斥变体,且每个变体可携带自己的 tuple、命名字段或无数据形状;枚举也能定义方法
Option<T>由Some(T)与None构成,使缺失值与普通 T 成为不同类型,使用内部值前必须处理缺失路径- match 是表达式,模式既检查形状又绑定数据;穷尽性检查确保所有可能值都有处理路径
- 捕获变量保留剩余值,
_忽略剩余值,()表示有意不执行动作;宽泛默认分支要谨慎使用 - if let 适合单模式分支,let...else 适合失败提前发散、成功绑定继续主路径
练习
- 问题 1:重构无效状态。 一个网络地址 struct 含
kind、v4: Option<[u8; 4]>和v6: Option<String>。它能产生哪些无效组合,如何用 enum 消除?
- 问题 2:完成匹配。 为
Option<i32>写一个函数:Some 中的正数加倍,零或负数保持原值,None 返回 Some(0)。为什么不能把Some(value)放在更具体 guard 或模式之前?
- 问题 3:选择控制流。 解析请求后必须取得
Some(user_id)才能继续,否则立即返回错误;另一个统计函数只在状态为 Active 时加一。两处分别选什么语法?
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 枚举
- 列出有限个互斥变体的自定义类型。
- 枚举变体
- 枚举的一种可能状态,通过枚举名限定构造。
- 关联数据
- 由特定枚举变体直接携带、匹配后才能访问的数据。
- Option
- 用 Some 或 None 显式表达值存在与缺失的标准库枚举。
- Some
- Option 中携带一个 T 值的变体。
- None
- Option 中表示没有值的变体。
- match 表达式
- 按顺序将值与模式 arm 比较并产生结果的控制流表达式。
- 模式
- 描述值的结构、常量或变体并可绑定组成部分的语法。
- 穷尽性
- match 必须覆盖被检查类型所有可能值的编译期要求。
- 捕获模式
- 匹配剩余任意值并把它绑定到变量的模式。
- if let
- 只处理一个目标模式、可带 else 的简洁控制流。
- let...else
- 成功绑定供后续使用、失败块必须发散的模式绑定语句。
来源与边界
本章结构、术语和示例边界对齐 The Rust Programming Language, Rust 2024 Edition 官方 Chapter 6 的 Enums and Pattern Matching、Defining an Enum、The match Control Flow Construct 与 Concise Control Flow with if let and let...else。交互实验和练习为本站重构,用于验证类型与控制流约束,不替代官方原文。