返回 Rust
Rust 2026-05-19

第6章 枚举和模式匹配

第6章 枚举和模式匹配

一、章节定位与核心目标

本章围绕 枚举(enum)模式匹配(pattern matching) 展开,目标是让我们掌握 Rust 中另一类核心自定义类型,以及围绕它展开的控制流写法。

这一章主要解决三个问题:

  1. 如何把“一组互斥的可能性”建模为类型。
  2. 如何让不同变体携带不同的数据。
  3. 如何在使用这些值时,安全、清晰、穷尽地处理所有情况。

如果说结构体更擅长表示“一个对象同时拥有若干字段”,那么枚举更擅长表示:

  • 一个值只能是若干种状态中的某一种;
  • 不同状态可能携带不同的数据;
  • 程序必须根据当前状态做出不同处理。

本章还引入了 Rust 标准库中极其重要的 Option<T>,它用类型系统表达“值可能存在,也可能不存在”,从而替代许多语言中的空值(null)机制,显著提升安全性。


二、什么是枚举

2.1 枚举的本质

枚举(enum)是一种列举某个类型所有可能取值的数据类型。

它适合描述这类问题:

  • 网络地址要么是 IPv4,要么是 IPv6;
  • 一条消息要么是退出、要么是移动、要么是写入文本、要么是改颜色;
  • 一个值要么存在(Some),要么不存在(None)。

这类问题的共同点是:

  • 值有有限种可能形态
  • 任一时刻只能处于其中一种形态;
  • 不同形态对应不同语义。

因此,枚举的关键作用不是“把数据并排放在一起”,而是“把若干互斥状态组织成一个统一类型”。


2.2 枚举与结构体的区别

结构体和枚举都属于自定义类型,但适用场景不同:

结构体适合表示:

“一个值同时拥有这些字段”。

例如矩形同时有:

  • width
  • height

枚举适合表示:

“一个值只能是这些变体之一”。

例如 IP 地址只能是:

  • V4
  • V6

所以可以概括为:

  • 结构体:并列组合多个属性
  • 枚举:在多个可能状态中二选一、多选一,但最终只取一种

三、枚举的定义与实例化

3.1 最基础的枚举定义

书中首先用 IP 地址类型举例:

enum IpAddrKind {
    V4,
    V6,
}

这个定义表示:IpAddrKind 类型的值只能是两种变体之一:

  • IpAddrKind::V4
  • IpAddrKind::V6

这里有几个关键点:

1)变体属于枚举的命名空间

使用时要写成:

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

其中 :: 表示“在某个类型或命名空间下访问成员”。

2)不同变体,统一属于同一个枚举类型

虽然 V4V6 看起来不同,但它们都是 IpAddrKind 类型。

这意味着可以写出接收 IpAddrKind 的函数:

fn route(ip_kind: IpAddrKind) {}

然后既能传 IpAddrKind::V4,也能传 IpAddrKind::V6

这体现了枚举的核心价值:

多个语义不同的状态,被统一纳入同一个类型系统中的“合法值集合”。


四、让枚举变体携带数据

仅有变体名字还不够,因为程序往往不仅要知道“是什么类型”,还要知道“具体数据是什么”。


4.1 结构体 + 枚举的组合方案

书中先展示了一种“结构体中嵌套枚举”的写法:

enum IpAddrKind {
    V4,
    V6,
}

struct IpAddr {
    kind: IpAddrKind,
    address: String,
}

然后可以构造:

let home = IpAddr {
    kind: IpAddrKind::V4,
    address: String::from("127.0.0.1"),
};

let loopback = IpAddr {
    kind: IpAddrKind::V6,
    address: String::from("::1"),
};

这种写法当然可行,它把:

  • 地址类型
  • 地址内容

打包进了同一个结构体中。

但这还不是最自然的 Rust 风格。


4.2 更典型的方式:让变体直接携带数据

Rust 更常见的写法是:

enum IpAddr {
    V4(String),
    V6(String),
}

实例化时:

let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));

这比“枚举 + 外部结构体字段”的方案更简洁,也更直接地表达了语义:

  • 一个 IpAddr 值本身就一个 IPv4 地址,或一个 IPv6 地址;
  • 每个变体都自带对应的数据。

4.3 变体名本身也是构造器

IpAddr::V4(...) 这样的写法,本质上可以看作:

  • V4IpAddr 的一个变体;
  • 同时,IpAddr::V4 也像一个函数,用来接收参数并构造该变体实例。

所以:

IpAddr::V4(String::from("127.0.0.1"))

可以理解为“调用变体构造器,得到一个 IpAddr 值”。


4.4 不同变体可以携带不同类型、不同数量的数据

这是枚举相对于结构体的一个巨大优势。

书中进一步把 IPv4 和 IPv6 分别设计成不同数据形式:

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

此时:

  • V4 携带四个 u8
  • V6 携带一个 String

这在结构体里很难优雅表达,因为结构体的字段布局对所有实例都是统一的,而枚举允许:

不同变体拥有完全不同的内部数据布局。

这意味着枚举不仅能表示“状态不同”,还能表示“不同状态下所需的数据结构本来就不同”。

这是 Rust 中非常强大的建模能力。


五、另一个典型例子:Message 枚举

书中给出了一个更有代表性的例子:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

这个例子展示了枚举变体的四种常见形态:

5.1 无数据变体

Quit

只表示一种状态,不携带额外信息。

5.2 类结构体变体

Move { x: i32, y: i32 }

带命名字段,适合表达语义清晰的多字段数据。

5.3 单值元组变体

Write(String)

适合表达“某个状态下只附带一个值”。

5.4 多值元组变体

ChangeColor(i32, i32, i32)

适合表达结构简单但需要多个同类值的数据。


5.5 为什么不直接定义多个结构体?

当然也可以分别定义:

  • QuitMessage
  • MoveMessage
  • WriteMessage
  • ChangeColorMessage

但这样做的问题是:

  • 它们是四个彼此独立的类型;
  • 很难统一写一个函数来处理“消息”这一整体概念;
  • API 层面会变得更零散。

而使用枚举时:

  • 这些不同形式都被统一归入 Message 类型;
  • 一个函数只需接收 Message,即可处理所有消息情形;
  • 类型模型更贴近“协议 / 命令 / 事件”这类实际业务结构。

这正是枚举在工程设计中非常重要的原因。


六、枚举也可以定义方法

枚举和结构体一样,也可以用 impl 定义方法:

impl Message {
    fn call(&self) {
        // 方法体
    }
}

然后可以这样调用:

let m = Message::Write(String::from("hello"));
m.call();

这说明 Rust 中方法并不是结构体专属能力,而是:

任何合适的自定义类型,只要语义上需要,都可以拥有方法。

因此,枚举不仅能存数据,还能承载行为。

这让我们能够把“状态”和“围绕状态的操作”组织到一起。


七、Option 枚举:Rust 中最重要的枚举之一

7.1 Option 要解决什么问题

现实编程中有一种极其常见的情况:

  • 这个值可能存在;
  • 也可能不存在。

例如:

  • 从非空列表取第一项,会得到值;
  • 从空列表取第一项,就得不到值;
  • 查表、查询、解析、配置读取时,也常常出现“可能有,也可能没有”的情况。

很多语言用 null 表示这种情形。但 null 最大的问题是:

  • 它很容易混入原本“应该始终有效”的值中;
  • 程序员常常忘记检查;
  • 一旦把空值当正常值使用,就会触发运行时错误。

Rust 没有采用这种机制,而是使用一个显式枚举来表达“有值 / 无值”。


7.2 Option 的定义

标准库中的定义是:

enum Option<T> {
    None,
    Some(T),
}

含义非常直接:

  • Some(T):存在一个值,类型为 T
  • None:不存在值

这里的 <T> 是泛型参数,表示:

  • Option 可以包裹任意类型;
  • Option<i32>Option<char>Option<String> 都是不同的具体类型。

7.3 Some 和 None 的使用方式

书中示例:

let some_number = Some(5);
let some_char = Some('e');
let absent_number: Option<i32> = None;

注意:

  • Some(5) 会推断为 Option<i32>
  • Some('e') 会推断为 Option<char>
  • 单独写 None 时,Rust 无法知道它缺失的到底是哪种类型,因此通常需要写出完整类型注解

也就是说:

None 不是“无类型的空”,而是“某个具体类型缺失了值”。

这在类型系统层面非常关键。


7.4 Option 为什么比 null 更安全

最核心的一点在于:

  • TOption<T>不同类型

例如:

  • i8 是普通整数
  • Option<i8> 是“可能为空的整数”

Rust 不允许把它们混着用。

书中给出例子:

let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;

这段代码不能编译。

原因不是 Rust “太严格”,而是它在强迫你面对一个本来就存在的事实:

y 可能没有值,所以你不能在没处理这种可能性的前提下,直接把它当普通整数使用。

因此,Rust 通过 Option<T> 做到了三件事:

  1. 把“可能为空”显式写进类型里;
  2. 让编译器强制你处理这种情况;
  3. 从根源上减少“忘记判空”这类错误。

这是 Rust 内存安全和类型安全思想的典型体现。


7.5 使用 Option 的基本原则

当你拿到一个 Option<T> 时,不应该直接把它当成 T 去使用,而应该先:

  • 匹配 Some / None
  • 或使用 Option 提供的方法

本章重点介绍的是第一种方式:match 做分支处理


八、match:枚举值最核心的处理方式

8.1 match 是什么

match 是 Rust 中非常强大的控制流结构。

它的作用是:

  • 把一个值与多个模式逐个比较;
  • 一旦匹配成功,就执行对应代码;
  • 并且要求你处理所有可能情况。

这使它特别适合处理枚举。


8.2 基本语法

书中用硬币示例说明:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

这里可以这样理解:

  • match coin:拿 coin 去匹配
  • 每个分支写一个模式
  • => 右边是该模式匹配成功时执行的表达式

8.3 match 是表达式

这一点很重要。

match 不是单纯的“语句”,它本身会产生一个值。因此:

  • 每个分支本质上都要返回同类结果;
  • 整个 match 表达式最终也会返回一个值。

在示例中,每个分支都返回 u8,所以整个 match 的结果也是 u8


8.4 分支代码可以是代码块

如果某个分支不只是一个简单值,也可以写成代码块:

Coin::Penny => {
    println!("Lucky penny!");
    1
}

这里最后一个表达式 1 仍然是该分支的返回值。

因此要牢记:

  • match 分支右侧既可以是简单表达式;
  • 也可以是块表达式;
  • 块中最后一个无分号表达式会成为该分支值。

九、在 match 中绑定变体内部的值

枚举真正强大的地方,不只是区分变体,还在于:

匹配时可以顺便把变体内部携带的数据取出来。

书中修改 Quarter 变体为:

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

然后在 match 中写:

Coin::Quarter(state) => {
    println!("State quarter from {state:?}!");
    25
}

这里的 state 不是提前定义好的变量,而是:

  • 模式匹配时新绑定的变量;
  • 它会接住 Quarter(...) 中的那个 UsState 值。

这意味着:

  • 匹配不仅是“判断是不是这个变体”;
  • 还是“如果是,就把里面的数据拆出来继续用”。

这是模式匹配的精髓之一。


十、使用 match 处理 Option<T>

10.1 示例:给 Option<i32> 中的值加一

书中给出:

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

这个函数表达得非常自然:

  • 如果没有值(None),那结果也没有值;
  • 如果有值(Some(i)),就把内部值 i 加一,再包装回 Some

这里体现了处理 Option<T> 的标准思路:

  1. 先区分是否存在值;
  2. 存在时再处理内部值;
  3. 不存在时明确写出对应逻辑。

10.2 为什么不能跳过 None

因为 Option<T> 的语义就是“可能没有值”。如果你只处理 Some 而不处理 None,逻辑就是不完整的。

Rust 会在编译期阻止这种疏漏。

这也是 match 最重要的特征之一:穷尽性检查


十一、match 的穷尽性:必须覆盖所有情况

11.1 什么叫穷尽

所谓穷尽(exhaustive),就是:

对一个值进行 match 时,必须覆盖该类型的一切可能情况。

例如 Option<i32> 只有两种情况:

  • Some(i32)
  • None

所以少写一个都不行。

书中错误示例:

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
    }
}

这里漏掉了 None,因此不能编译。

11.2 Rust 为什么这么要求

因为漏掉某种情况,往往意味着潜在 bug。

Rust 的做法是:

  • 不让你带着这种不完整逻辑继续往下走;
  • 在编译时强制你补全所有分支。

这和许多语言“运行时出问题再说”的风格完全不同。

从长期工程角度看,这种强约束非常值钱,因为它把很多错误前移到了编译阶段。


十二、通配模式 _ 与默认分支

12.1 通配分支的作用

有时你只关心少数几个特殊值,其他值都想交给默认逻辑处理。

这时可以使用通配模式:

match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    other => move_player(other),
}

这里:

  • 37 被单独处理;
  • 其余值都匹配到 other
  • other 还会把该值绑定出来供后续使用。

12.2 _:匹配但不绑定

如果你根本不需要默认分支中的那个值,就用 _

match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    _ => reroll(),
}

_ 的含义是:

  • 匹配任意剩余情况;
  • 但不把值绑定到变量。

这有两个好处:

  1. 表意明确:我确实不关心这个值;
  2. 不会触发“未使用变量”的警告。

12.3 _ => ():显式表示“不做任何事”

如果默认情况是什么也不做,可以写:

match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    _ => (),
}

这里的 () 是单元值,表示:

  • 该分支匹配到了;
  • 但没有额外动作,也没有有意义的值产出。

这是一种很典型的 Rust 写法。


十三、if let:只关心一种匹配时的简化写法

13.1 为什么需要 if let

有时使用 match 会显得啰嗦,尤其是当我们:

  • 只关心某一个模式;
  • 其他所有情况都想忽略。

书中例子:

let config_max = Some(3u8);
match config_max {
    Some(max) => println!("The maximum is configured to be {max}"),
    _ => (),
}

逻辑上很简单:

  • 若是 Some(max),打印;
  • 否则什么也不做。

但为了满足 match 的穷尽性,还得额外写 _ => ()

这时就可以改写为:

let config_max = Some(3u8);
if let Some(max) = config_max {
    println!("The maximum is configured to be {max}");
}

13.2 if let 的本质

可以把它理解为:

只保留一个分支的 match 的语法糖。

写法形式是:

if let 模式 = 表达式 {
    // 匹配成功时执行
}

它做的事和 match 类似:

  • 若表达式匹配该模式,则进入代码块;
  • 并把模式中绑定的变量引入代码块中;
  • 否则直接跳过。

13.3 if let 的优缺点

优点

  • 更简洁
  • 更少样板代码
  • 适合“只关心一种情况”的逻辑

代价

  • 失去 match 那种强制穷尽性检查
  • 可能让你忽略其他情况而不自知

所以原则是:

  • 需要完整分支逻辑时,用 match
  • 只关心一种情况时,用 if let

这不是功能强弱之分,而是表达意图的选择。


十四、if let 配合 else

如果你关心一种情况,同时希望其他情况执行另一段默认逻辑,就可以使用 if let ... else

书中示例把:

match coin {
    Coin::Quarter(state) => println!("State quarter from {state:?}!"),
    _ => count += 1,
}

改写为:

if let Coin::Quarter(state) = coin {
    println!("State quarter from {state:?}!");
} else {
    count += 1;
}

这说明:

  • if let 不是只能“匹配成功就执行”;
  • 也可以带 else,专门处理“匹配失败”的统一逻辑。

不过它仍然只适合“一个重点分支 + 一个兜底分支”的场景。

如果分支开始变多,还是应回到 match


十五、let…else:让主流程保持顺畅

15.1 它要解决的问题

在实际代码中,经常出现这种模式:

  • 如果匹配成功,就继续主逻辑;
  • 如果匹配失败,就立刻返回。

这类代码用 if let 能写,但容易把“真正的主流程”塞进 if 块里,导致代码向右偏、层级变深,不利于阅读。

书中示例先给出了这种 if let 写法:

fn describe_state_quarter(coin: Coin) -> Option<String> {
    if let Coin::Quarter(state) = coin {
        if state.existed_in(1900) {
            Some(format!("{state:?} is pretty old, for America!"))
        } else {
            Some(format!("{state:?} is relatively new."))
        }
    } else {
        None
    }
}

逻辑正确,但主线被包进了 if let 块中。


15.2 用 if let 提前返回的改写

书中进一步改写成:

let state = if let Coin::Quarter(state) = coin {
    state
} else {
    return None;
};

这已经更接近我们真正想表达的意思:

  • 若匹配成功,提取 state
  • 若失败,立刻返回 None

但这种写法仍然稍显笨重。


15.3 let…else 的形式

因此 Rust 提供了 let...else

fn describe_state_quarter(coin: Coin) -> Option<String> {
    let Coin::Quarter(state) = coin else {
        return None;
    };

    if state.existed_in(1900) {
        Some(format!("{state:?} is pretty old, for America!"))
    } else {
        Some(format!("{state:?} is relatively new."))
    }
}

它的语义非常清晰:

  • 左边写“我希望匹配到的模式”;
  • 右边写“要进行匹配的表达式”;
  • 若匹配成功,把绑定值带到外层作用域;
  • 若匹配失败,进入 else,并且 else 必须中断当前流程(如 return)。

15.4 let…else 的最大价值

它最大的价值在于:

让“失败分支”尽早退出,让“主逻辑”保持在线性、顺畅的 happy path 上。

这是一种非常实用的代码组织思想:

  • 先把不满足条件的情况尽早排除;
  • 然后让真正要做的事自然往下写。

因此,let...else 特别适合:

  • 参数校验
  • 模式解构后继续主流程
  • 需要“匹配失败就立即返回”的场景

它提升的不是功能,而是代码的可读性和控制流清晰度


十六、本章的思想主线

把全章串起来,可以看到它并不是在零散地介绍几个语法,而是在逐步建立一种 Rust 风格的数据建模和控制流思维。

16.1 第一步:用枚举表达“互斥状态”

  • 一个值只能是若干种状态之一;
  • 不同状态可以附带不同数据;
  • 这使建模更贴近真实问题本身。

16.2 第二步:用 Option<T> 显式表达“可能缺失”

  • 不再依赖隐式的 null
  • 把“是否存在值”纳入类型系统;
  • 迫使程序员显式处理缺失情况。

16.3 第三步:用 match 做穷尽而安全的分支处理

  • 每一种可能情况都必须考虑;
  • 匹配时还能解构出内部值;
  • 编译器为完整性兜底。

16.4 第四步:在简洁性和完整性之间做选择

  • match:完整、明确、适合多分支
  • if let:简洁、适合单重点分支
  • let...else:适合提前退出并保持 happy path

这三者共同构成了 Rust 中围绕枚举的主流控制流写法。


十七、易混点与学习重点

17.1 枚举不是“常量列表”

很多初学者会把枚举理解成“几个名字的集合”。这只说对了一半。

在 Rust 中,枚举远不止是标签集合,它还可以:

  • 携带数据;
  • 每个变体携带不同类型的数据;
  • 定义方法;
  • 配合模式匹配直接解构内部内容。

所以 Rust 的枚举更接近“代数数据类型”的思想,而不仅仅是传统语言里那种整数映射。


17.2 Option<T> 不是 T

这是本章必须牢牢记住的一点。

  • T 表示“一定有值”
  • Option<T> 表示“可能有值,也可能没值”

不能把它们混用,也不能跳过缺失情况直接使用。


17.3 match 的重点不只是分支,而是“解构 + 穷尽”

match 的强大不在于它像 switch,而在于它能够:

  • 根据模式分类;
  • 解构出内部数据;
  • 保证没有漏处理的情况。

这是 Rust 控制流中最具代表性的特征之一。


17.4 if let 是简写,不是替代品

if let 很方便,但不能把它当作 match 的完全替代。

当你需要:

  • 多分支逻辑
  • 完整覆盖所有情况
  • 更强的可读性和显式性

依然应该优先考虑 match


17.5 let...else 的关键在控制流设计

它不是单纯为了少写几行代码,而是为了让:

  • 失败路径尽早退出;
  • 主流程保持平直清晰。

这是 Rust 在语法层面对工程可读性的支持。


十八、本章精华总结

18.1 枚举的核心价值

枚举用于定义“一个值只能是若干种可能状态之一”的类型,并且每种状态都可以携带不同的数据。

18.2 Rust 枚举比传统枚举更强

Rust 的枚举不仅能列出状态,还能让变体携带数据、具有不同结构,并与方法和模式匹配结合。

18.3 Option<T> 是 Rust 处理“值可能不存在”的标准方案

它替代了空值机制,把“可能为空”显式纳入类型系统,避免大量运行时错误。

18.4 match 是处理枚举的标准工具

它能够根据变体分支、提取内部数据,并通过穷尽性检查保证逻辑完整。

18.5 _ 用于默认匹配,if let 用于单分支简化,let...else 用于提前退出并保持主路径清晰

这三者是 Rust 枚举控制流中的常用表达方式,应根据代码意图灵活选择。


十九、学习完成后的能力清单

学完本章后,应该能够做到:

  • 理解结构体与枚举各自适合表达的问题类型;
  • 定义简单枚举与带数据的枚举;
  • 明白不同变体可以携带不同数量和类型的数据;
  • 理解 Option<T> 的意义以及它与空值机制的本质区别;
  • 使用 match 处理枚举,并在分支中绑定内部值;
  • 理解 match 的穷尽性要求;
  • 正确使用 _ 作为通配模式;
  • 在只关心一个分支时使用 if let
  • 在“匹配失败则提前返回”的场景中使用 let...else

二十、一句话收束本章

结构体回答的是“这个值同时有哪些字段”,枚举回答的是“这个值当前是哪一种状态”;而 matchif letlet...else,则是 Rust 用来安全而清晰地处理这些状态的核心工具。