编程语言研究书库

枚举与模式匹配:把"可能性"编进类型 — 第 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 pattern

match 是穷尽的:你必须覆盖所有可能性,代码才有效。 编译器不但发现你漏了,还知道漏的是哪个——在 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. 可带走的

  1. 枚举的变体能挂不同类型/数量的数据;变体名自动是构造函数。
  2. 想"一个函数接受几种消息"时,一个枚举比几个结构体好——变体共享同一类型。
  3. Rust 没有 null;Option<T> 的 None/Some 是它的替代品,且 Some/None 已进 prelude。
  4. Option<T> 与 T 是不同类型,混用直接编译错——"可能为空"必须显式声明。
  5. match 按模式顺序匹配,臂的值即整个 match 的值;模式里的变量可以绑定变体内部数据。
  6. match 穷尽:漏分支,E0004;通配臂最后放。
  7. _ 匹配一切但不绑定;_ => () 表示"其余啥也不干"。
  8. 只关心一种情况用 if let;代价是失去穷尽性检查。
  9. "取不到就退出"用 let...else,else 必须发散。

8. 原文地图

主题原书章原文位置
枚举 vs 结构体+标志位Chapter 6: Enums and Pattern Matchingtext/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

  1. 出处:「Chapter 6: Enums and Pattern Matching」第 69 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:69,搜「enum IpAddrKind」)。 ↩

  2. 出处:「Chapter 6: Enums and Pattern Matching」第 118 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:118,搜「constructor function」)。 ↩

  3. 出处:「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)」)。 ↩

  4. 出处:「Chapter 6: Enums and Pattern Matching」第 168 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:168,搜「enum Message」)。 ↩

  5. 出处:「Chapter 6: Enums and Pattern Matching」第 197 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:197,搜「couldn’t as easily」)。 ↩

  6. 出处:「Chapter 6: Enums and Pattern Matching」第 203 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:203,搜「impl Message」)。 ↩

  7. 出处:「Chapter 6: Enums and Pattern Matching」第 229 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:229,搜「features you exclude」)。 ↩ ↩2

  8. 出处:「Chapter 6: Enums and Pattern Matching」第 233 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:233,搜「Billion Dollar Mistake」)。 ↩ ↩2

  9. 出处:「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」)。 ↩

  10. 出处:「Chapter 6: Enums and Pattern Matching」第 306 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:306,搜「always have a valid value」)。 ↩

  11. 出处:「Chapter 6: Enums and Pattern Matching」第 320 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:320,搜「design decision」)。 ↩

  12. 出处:「Chapter 6: Enums and Pattern Matching」第 337 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:337,搜「extremely powerful control flow」)。 ↩

  13. 出处:「Chapter 6: Enums and Pattern Matching」第 344 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:344,搜「coin-sorting machine」)。 ↩

  14. 出处:「Chapter 6: Enums and Pattern Matching」第 389 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:389,搜「value of the expression」)。 ↩

  15. 出处:「Chapter 6: Enums and Pattern Matching」第 409 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:409,搜「Bind to Values」)。 ↩

  16. 出处:「Chapter 6: Enums and Pattern Matching」第 474 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:474,搜「plus_one」)。 ↩

  17. 出处:「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

  18. 出处:「Chapter 6: Enums and Pattern Matching」第 578 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:578,搜「special pattern」)。 ↩

  19. 出处:「Chapter 6: Enums and Pattern Matching」第 575 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:575,搜「put the catch-all arm last」)。 ↩

  20. 出处:「Chapter 6: Enums and Pattern Matching」第 653 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:653,搜「lose the exhaustive checking」)。 ↩ ↩2

  21. 出处:「Chapter 6: Enums and Pattern Matching」第 661 段(text/13-ch06-chapter-6-enums-and-pattern-matching.txt:661,搜「include an else」)。 ↩

  22. 出处:「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」)。 ↩

On this page