枚举与模式匹配:把"可能性"编进类型 — 第 6 章拆解
枚举与模式匹配:把"可能性"编进类型
这一章讲什么: 枚举——一个值"只能是若干可能之一"的类型;以及它的搭档 match—— 按可能性分派代码,且编译器检查你有没有漏掉任何一种。 读完你要能解释:Rust 没有 null,取而代之的是
Option<T>; 为什么Option比 null 安全;以及 match / if let / let...else 三件套各自什么时候用。 这一章与第 5 章合起来,给了你"造领域类型"的完整工具箱。
1. 顶层全景:枚举比"结构体 + 标志位"强在哪
主线例子:IP 地址。
朴素做法(结构体套枚举):一个 kind: IpAddrKind 字段标记是 V4 还是 V6,一个 address: String 字段存地址。能用,但两个字段的关联完全靠自觉1。
枚举做法:把数据直接放进变体。
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);两处改进:
- 省掉外层的结构体;而且定义枚举时,每个变体名自动变成一个"构造函数"——
IpAddr::V4就是一个接受四个 u8、返回 IpAddr 的函数2。 - 每个变体可以挂不同类型、不同数量的数据。 V4 是四个 u8、V6 是一个 String——结构体做不到,枚举轻松。甚至可以挂结构体、挂另一个枚举。标准库的 IpAddr 就是
V4(Ipv4Addr)/V6(Ipv6Addr),结构和你自己想的一样3。
Message 例子展示了四种变体形态全集:无数据(Quit)、命名字段(Move { x, y },像结构体)、单个值(Write(String))、多个值(ChangeColor(i32, i32, i32))4。对比"为每种消息建一个结构体":后者每种消息是独立类型,你没法轻易写一个"接受任意消息"的函数;枚举的四个变体同属一个类型 Message5。和结构体一样,枚举也能定义方法(impl Message { fn call(&self) {} })6。
2. Option<T>:Rust 对 null 的正式回答
语言设计不只看加入了什么特性,也要看排除了什么。Rust 没有 null。7
书在这里引了 Tony Hoare 的原话:他称 null 为"十亿美元的错误"——本想造一个所有引用都绝对安全的类型系统,没忍住放进了 null 引用,四十年间造成了数不清的错误、漏洞和系统崩溃8。null 的问题不在概念("目前没有有效值"是个有用的概念),而在实现:值总是"null 或非 null"两种状态,用起来极易把 null 当非 null 用。
Rust 的替代:
enum Option<T> {
None,
Some(T),
}Option<T> 就是一个普通枚举:None 表示"没有值",Some(T) 表示"有一个 T 类型的值"。
它在 prelude 里,Some 和 None 不用加 Option:: 前缀。<T> 是泛型参数(第 10 章):Some 可以装任意类型,Some(5) 是 Option<i32>、Some('e') 是 Option<char>,是不同的类型;光秃秃的 None 必须标注 Option<i32> 这样的类型,编译器光凭 None 推不出 T9。
为什么这比 null 安全:因为 Option<i8> 和 i8 是不同类型,编译器不让你混用。
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y; // error: cannot add `Option<i8>` to `i8`类型的 i8 保证"一定有有效值",用之前不用查 null;Option 才需要担心"可能没有",而编译器会强制你先处理这个可能,再让你用里面的值10。一句话:想"可空",必须显式声明成 Option<T>;不是 Option 的地方,可以放心假定它非空。书说这是刻意的设计决策11。
3. match:按模式分派,且不许漏
match 把值和一连串模式比较,命中哪个执行哪段代码。 与 if 的关键区别:if 的条件必须是 bool,match 的值可以是任何类型12。书里的比方:分拣硬币的机器——硬币沿轨道滚,遇到第一个合适的孔就掉下去13。
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}臂(arm)= 模式 + => + 代码;按顺序匹配,命中即止;每段臂代码是表达式,它的值就是整个 match 的值(所以 match 也能出现在 let 右边)。臂代码多于一行要用花括号,那时逗号可省14。
3.1 绑定:从变体里把数据掏出来
Coin::Quarter(UsState) 这种带数据的变体,匹配时可以在模式里放变量,把内部数据绑出来:
match coin {
Coin::Quarter(state) => println!("State quarter from {state:?}!"),
_ => ...
}调用 value_in_cents(Coin::Quarter(UsState::Alaska)) 时,state 绑定到 UsState::Alaska,之后臂里随便用15。
把 match 用到 Option 上,主线例子变成 plus_one:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}plus_one(Some(5)):Some(5) 不匹配 None,落到 Some(i),i 绑定 5,臂的值 Some(6) 就是结果;plus_one(None):第一条臂直接命中,返回 None16。
3.2 穷尽性:漏一个分支,编译不过
只写 Some(i) 臂、漏掉 None:
error[E0004]: non-exhaustive patterns: `None` not covered
help: ensure that all possible cases are being handled by adding a
match arm with a wildcard pattern or an explicit patternmatch 是穷尽的:你必须覆盖所有可能性,代码才有效。 编译器不但发现你漏了,还知道漏的是哪个——在 Option 上,这就直接消灭了 Hoare 那类"误以为有值"的错误17。
通配模式(other)与占位符(_):只特殊处理少数值、其余走默认时,用变量名做"接住所有剩余值"的臂,还能顺手绑定值(other => move_player(other));不想用那个值就用 _ => ...,它匹配一切但不绑定——既满足穷尽性,也不产生"未使用变量"警告。_ => () 表示"其余情况什么都不做"18。通配臂必须放最后,否则后面的臂永远到不了,Rust 会警告19。
4. if let 与 let...else:只关心一种情况的简写
match 的代价:有时你只在乎一种情况,却得为穷尽性补一个 _ => () 样板臂,烦。
if let:只匹配一个模式、忽略其余。 下面两段等价:
match config_max {
Some(max) => println!("The maximum is configured to be {max}"),
_ => (),
}
if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
}代价书里说得很明白:你失去了 match 的穷尽性检查——选择 match 还是 if let,就是"完整检查"换"简洁"的权衡20。可以配 else:else 块等价于 match 的 _ 臂21。
let...else:新模式绑不上就早退。 常见写法是"值在就干活、不在就 return None",用 if let 写要把主线逻辑塞进 if 块里,嵌套发闷(第 6 节例子)。let...else 把这个模式翻转过来:
fn describe_state_quarter(coin: Coin) -> Option<String> {
let Coin::Quarter(state) = coin else {
return None;
};
// 从这里往下,state 一定存在——主线逻辑不再嵌套
...
}匹配成功:变量绑定到外层作用域;匹配失败:走 else,而 else 必须以某种方式离开当前函数(return / break / panic)。 效果是函数主体留在"快乐路径"(happy path)上22。
三件套怎么选:
| 场景 | 用 |
|---|---|
| 要处理大部分/全部情况 | match(白得穷尽性检查) |
| 只在乎一种情况,其余忽略 | if let(+ else) |
| "取不到就退出,取到就继续主线" | let...else |
5. 作者的判断与证据
- "排除 null 是重要的设计"——引 Hoare 的"十亿美元错误"作为历史证据,这是作者立场最鲜明的一段78。
- "match 穷尽性让 billion-dollar mistake 不可能"——机制论证:None 不可漏17。
- "if let 换简洁、失穷尽检查"——书中明示的权衡,教读者自己选20。
判断(我们的,不是书里的): Option 真正的力量不在类型本身,而在它和 match 穷尽性组合后形成的一套纪律: "可空"是类型签名里看得见的承诺,处理它是编译器强制的义务。 这套纪律后来被整个生态外推——
Result是同一个模式的错误版(第 9 章), 许多 API 宁可返回 Option 也不要默认值或异常。 如果错,会错在: 纪律有成本——层层 Option/Result 的处理代码会让简单逻辑变啰嗦 (这正是?、if let、let...else不断加入语言的原因,见第 9、13 章); 对"快速写一次就扔"的脚本,这套强制可能得不偿失。
6. 边界与局限
- 模式的全部种类(解构结构体、守卫、@ 绑定等)留给第 19 章。
- Option 的方法族(map、unwrap_or 等)只提了一句"看文档",没有展开。
- let...else 的 else 必须发散(diverge)这一约束,书中以"the program will flow into the else arm, which must return from the function"表述,例子只给了 return。
7. 可带走的
- 枚举的变体能挂不同类型/数量的数据;变体名自动是构造函数。
- 想"一个函数接受几种消息"时,一个枚举比几个结构体好——变体共享同一类型。
- Rust 没有 null;
Option<T>的 None/Some 是它的替代品,且 Some/None 已进 prelude。 Option<T>与 T 是不同类型,混用直接编译错——"可能为空"必须显式声明。- match 按模式顺序匹配,臂的值即整个 match 的值;模式里的变量可以绑定变体内部数据。
- match 穷尽:漏分支,E0004;通配臂最后放。
_匹配一切但不绑定;_ => ()表示"其余啥也不干"。- 只关心一种情况用 if let;代价是失去穷尽性检查。
- "取不到就退出"用 let...else,else 必须发散。
8. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 枚举 vs 结构体+标志位 | Chapter 6: Enums and Pattern Matching | text/13-ch06-chapter-6-enums-and-pattern-matching.txt:14(搜「concise」) |
| 变体即构造函数 | 同上 | text/13-ch06-chapter-6-enums-and-pattern-matching.txt:118(搜「constructor function」) |
| Message 四形态 | 同上 | text/13-ch06-chapter-6-enums-and-pattern-matching.txt:168(搜「enum Message」) |
| 没有 null 的设计 | 同上 | text/13-ch06-chapter-6-enums-and-pattern-matching.txt:229(搜「features you exclude」) |
| 十亿美元错误 | 同上 | text/13-ch06-chapter-6-enums-and-pattern-matching.txt:233(搜「Billion Dollar Mistake」) |
| Option 定义与 prelude | 同上 | text/13-ch06-chapter-6-enums-and-pattern-matching.txt:254(搜「enum Option」) |
| Option 与 T 不同型 | 同上 | text/13-ch06-chapter-6-enums-and-pattern-matching.txt:296(搜「cannot add」) |
| match 与硬币分拣机 | 同上 | text/13-ch06-chapter-6-enums-and-pattern-matching.txt:344(搜「coin-sorting machine」) |
| 模式绑定 | 同上 | text/13-ch06-chapter-6-enums-and-pattern-matching.txt:409(搜「Bind to Values」) |
| plus_one 与穷尽性 | 同上 | text/13-ch06-chapter-6-enums-and-pattern-matching.txt:527(搜「non-exhaustive patterns」) |
通配与 _ | 同上 | text/13-ch06-chapter-6-enums-and-pattern-matching.txt:578(搜「special pattern」) |
| if let 与 let...else | 同上 | text/13-ch06-chapter-6-enums-and-pattern-matching.txt:620(搜「if let」) · text/13-ch06-chapter-6-enums-and-pattern-matching.txt:683(搜「let...else」) |
Footnotes
-
出处:「Chapter 6: Enums and Pattern Matching」第 69 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:69,搜「enum IpAddrKind」)。 ↩ -
出处:「Chapter 6: Enums and Pattern Matching」第 118 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:118,搜「constructor function」)。 ↩ -
出处:「Chapter 6: Enums and Pattern Matching」第 128 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:128,搜「V4(u8, u8, u8, u8)」)与第 153 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:153,搜「V4(Ipv4Addr)」)。 ↩ -
出处:「Chapter 6: Enums and Pattern Matching」第 168 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:168,搜「enum Message」)。 ↩ -
出处:「Chapter 6: Enums and Pattern Matching」第 197 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:197,搜「couldn’t as easily」)。 ↩ -
出处:「Chapter 6: Enums and Pattern Matching」第 203 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:203,搜「impl Message」)。 ↩ -
出处:「Chapter 6: Enums and Pattern Matching」第 229 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:229,搜「features you exclude」)。 ↩ ↩2 -
出处:「Chapter 6: Enums and Pattern Matching」第 233 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:233,搜「Billion Dollar Mistake」)。 ↩ ↩2 -
出处:「Chapter 6: Enums and Pattern Matching」第 254 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:254,搜「enum Option」)与第 277 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:277,搜「requires us to annotate」)。 ↩ -
出处:「Chapter 6: Enums and Pattern Matching」第 306 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:306,搜「always have a valid value」)。 ↩ -
出处:「Chapter 6: Enums and Pattern Matching」第 320 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:320,搜「design decision」)。 ↩ -
出处:「Chapter 6: Enums and Pattern Matching」第 337 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:337,搜「extremely powerful control flow」)。 ↩ -
出处:「Chapter 6: Enums and Pattern Matching」第 344 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:344,搜「coin-sorting machine」)。 ↩ -
出处:「Chapter 6: Enums and Pattern Matching」第 389 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:389,搜「value of the expression」)。 ↩ -
出处:「Chapter 6: Enums and Pattern Matching」第 409 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:409,搜「Bind to Values」)。 ↩ -
出处:「Chapter 6: Enums and Pattern Matching」第 474 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:474,搜「plus_one」)。 ↩ -
出处:「Chapter 6: Enums and Pattern Matching」第 527 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:527,搜「non-exhaustive patterns」)与第 543 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:543,搜「every last possibility」)。 ↩ ↩2 -
出处:「Chapter 6: Enums and Pattern Matching」第 578 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:578,搜「special pattern」)。 ↩ -
出处:「Chapter 6: Enums and Pattern Matching」第 575 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:575,搜「put the catch-all arm last」)。 ↩ -
出处:「Chapter 6: Enums and Pattern Matching」第 653 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:653,搜「lose the exhaustive checking」)。 ↩ ↩2 -
出处:「Chapter 6: Enums and Pattern Matching」第 661 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:661,搜「include an else」)。 ↩ -
出处:「Chapter 6: Enums and Pattern Matching」第 745 段(
text/13-ch06-chapter-6-enums-and-pattern-matching.txt:745,搜「an else branch」)与第 765 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:765,搜「happy path」)。 ↩