Skip to content
Published at:

10. Enums & Patterns — 枚举与模式匹配

如果说结构体是“把几个值放在一起(and)”,那枚举就是“从几个可能性中选一个(or)”。Rust 的枚举是代数数据类型(Algebraic Data Type)的实现——每个变体可以携带不同类型和数量的数据。配合模式匹配,枚举构成了 Rust 中最强大的控制流工具。

计算机科学大师 Donald Knuth 曾写道:“处理非均匀结构是计算机科学的关键特征,而数学家则寻找统一的公理。”枚举正是 Rust 处理“非均匀结构”的核心机制。

枚举的定义

枚举声明列举出该类型可能的所有取值——每个取值称为一个变体(variant):

rust
enum WebEvent {
    PageLoad,                       // 单元变体
    KeyPress(char),                 // 元组变体
    Click { x: i64, y: i64 },       // 结构体变体
    Paste(String),                  // 元组变体
}

枚举的值一定是刚好一个变体——不可能同时是 PageLoad 又是 Click。这是枚举与结构体最本质的区别:结构体同时拥有所有字段,枚举只持有当前变体的数据。

枚举的内存布局

在内存中,枚举需要足够大的空间容纳最大的变体,外加一个标记(tag)区分当前是哪个变体:

rust
enum RoughTime {
    InThePast(WebEvent, String),
    JustNow,
    InTheFuture(WebEvent, String),
}

// 内存布局(示意):
// [tag: 8字节][WebEvent: 最大变体大小][String: 24字节]

Rust 对枚举的优化非常激进——在多种情况下,编译器可以利用“禁止值”来省略标记字节。例如 Option<Box<T>>None 时的空指针来表示,不需要额外的标记。

创建和使用枚举值

rust
// 创建枚举值
let page_load = WebEvent::PageLoad;
let key_press = WebEvent::KeyPress('x');
let click = WebEvent::Click { x: 20, y: 80 };
let paste = WebEvent::Paste("Hello".to_string());

Option<T>:可能没有值的值

Option<T> 是 Rust 标准库中最常用的枚举——它表达了“要么有值,要么没有”的语义:

rust
// 标准库中的定义
enum Option<T> {
    None,
    Some(T),
}

Option<T>T不同的类型——你不能在期望 T 的地方直接传 Option<T>,反之亦然。这个设计消除了空指针问题——Tony Hoare 称空指针是他“十亿美元的错误”,Rust 用类型系统彻底解决了它。

rust
fn divide(numerator: f64, denominator: f64) -> Option<f64> {
    if denominator == 0.0 {
        None
    } else {
        Some(numerator / denominator)
    }
}

// 调用者必须处理两种情况
match divide(10.0, 2.0) {
    Some(result) => println!("结果:{result}"),
    None => println!("除数不能为零"),
}

Option<T> 的常用方法:

方法签名行为
is_some()&self -> bool是否为 Some
is_none()&self -> bool是否为 None
unwrap()self -> T提取 Some 的值,None 时 panic
unwrap_or(default)self -> T提取值或返回默认值
unwrap_or_else(f)self -> T提取值或用闭包生成默认值
map(f)self -> Option<U>Some 的值应用函数
and_then(f)self -> Option<U>链式操作,f 返回 Option
expect(msg)self -> Tunwrap,但带自定义 panic 消息
rust
let config_override = Some("localhost");

// 链式处理
let port = config_override
    .and_then(|host| lookup_port(host))
    .unwrap_or(8080);

Result<T, E>:可能失败的操作

Result<T, E> 是 Rust 的错误处理核心——它表达“要么成功得到 T,要么失败得到错误 E”:

rust
// 标准库中的定义
enum Result<T, E> {
    Ok(T),
    Err(E),
}

Option 不同,Result 的失败分支携带了“为什么失败”的信息:

rust
use std::fs::File;
use std::io::Read;

fn read_file(path: &str) -> Result<String, std::io::Error> {
    let mut file = File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

Result 的常用方法:

方法行为
is_ok() / is_err()检查状态
unwrap()提取 Ok 值,Err 时 panic
unwrap_err()提取 Err 值,Ok 时 panic
expect(msg)unwrap,自定义消息
map(f)Ok 值应用函数
map_err(f)Err 值应用函数
and_then(f)链式操作(f 返回 Result

Result 在 Rust 中比 Option 更常用——Rust 没有异常,所有可能失败的操作都返回 Result

match 表达式

match 是 Rust 中最重要的控制流结构——它穷举所有可能情况,不遗漏任何一个:

rust
match value {
    pattern1 => expression1,
    pattern2 => expression2,
    pattern3 => {          // 大括号支持多条语句
        statement1;
        statement2;
        expression3
    }
}

穷举性(Exhaustiveness)

编译器强制 match 覆盖所有可能的变体:

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

fn value_in_cents(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
        // 如果注释掉任一行,编译器报错
    }
}

这是 Rust 类型安全的重要保障——当你给枚举添加新变体时,编译器会告诉你所有需要更新的 match 表达式。

通配符和 _ 占位符

不需要使用匹配值时,用 _ 忽略:

rust
fn is_nickel_or_dime(coin: Coin) -> bool {
    match coin {
        Coin::Nickel | Coin::Dime => true,  // | 表示"或"
        _ => false,                          // 匹配所有其他情况
    }
}

_ 不绑定值——如果匹配了多个 _ 分支,编译器无法确定用哪个值。需要绑定但忽略名称时用变量:

rust
match something {
    Some(value) => process(value),
    other => {          // other 绑定了匹配的值
        println!("未处理的值:{other:?}");
        default_value
    }
}

模式语法大全

Rust 的模式匹配语法极为丰富,下面逐一介绍。

字面量模式

rust
match x {
    0 => "零",
    1 => "一",
    2 | 3 => "二或三",  // | 匹配多个值
    _ => "其他",
}

变量绑定

rust
match opt {
    Some(v) => println!("值为:{v}"),  // v 绑定 Some 内的值
    None => println!("无值"),
}

解构结构体

rust
struct Point { x: i32, y: i32 }

let p = Point { x: 10, y: 20 };
match p {
    Point { x, y: 0 } => println!("在 x 轴上,x = {x}"),
    Point { x: 0, y } => println!("在 y 轴上,y = {y}"),
    Point { x, y } => println!("点 ({x}, {y})"),
}

y: 0 表示“匹配 y 字段为 0 的情况”,而 y 表示“绑定 y 字段到变量 y”。

解构枚举

rust
match event {
    WebEvent::PageLoad => println!("页面加载"),
    WebEvent::KeyPress(c) => println!("按键:{c}"),
    WebEvent::Click { x, y } => println!("点击 ({x}, {y})"),
    WebEvent::Paste(s) if s.len() > 0 => println!("粘贴了 {} 个字符", s.len()),
    WebEvent::Paste(_) => println!("空粘贴"),
}

匹配守卫(Match Guards)

if 条件可以进一步过滤模式:

rust
match number {
    n if n < 0 => println!("负数"),
    n if n % 2 == 0 => println!("偶数"),
    n => println!("正奇数:{n}"),
}

// 复杂守卫
match point {
    Point { x, y } if x == y => println!("在对角线上"),
    Point { x, y } if x > 5 && y < 10 => println!("在指定区域内"),
    _ => println!("其他位置"),
}

@ 绑定

@ 让你在解构的同时绑定整个值:

rust
enum Message {
    Hello { id: i32 },
}

match msg {
    Message::Hello {
        id: id_variable @ 3..=7,
    } => println!("id 在 3 到 7 之间:{id_variable}"),
    Message::Hello { id: 10..=12 } => {
        // 没有 @,只知道 id 在 10..12 但不知道具体值
        println!("id 在 10 到 12 之间");
    }
    Message::Hello { id } => println!("id 为 {id}"),
}

范围模式

..= 匹配一个闭区间(包含两端):

rust
match x {
    0..=5 => println!("0-5"),
    6..=10 => println!("6-10"),
    _ => println!("其他"),
}

// 对字符也有效
match ch {
    'a'..='j' => println!("早期 ASCII 字母"),
    'k'..='z' => println!("后期 ASCII 字母"),
    _ => println!("不是小写字母"),
}

引用模式

refref mut 在匹配时借用而非移动:

rust
match value {
    ref r => println!("借用引用:{r:?}"),  // r: &T
}

match value {
    ref mut r => *r += 1,              // r: &mut T
}

现代 Rust 中,ref 的使用已经减少——更多使用 & 模式匹配引用。

切片模式

rust
fn describe_slice(names: &[&str]) {
    match names {
        [] => println!("空列表"),
        [single] => println!("仅有一个:{single}"),
        [first, second] => println!("前两个:{first} 和 {second}"),
        [first, .., last] => println!("从 {first} 到 {last}"),
    }
}

if let 和 while let

当只关心一个变体时,if letmatch 更简洁:

rust
// 使用 match(啰嗦)
match option_value {
    Some(value) => println!("值:{value}"),
    _ => {}  // 什么都不做
}

// 使用 if let(简洁)
if let Some(value) = option_value {
    println!("值:{value}");
}

// 带 else
if let Some(value) = option_value {
    println!("值:{value}");
} else {
    println!("无值");
}

while let 持续匹配直到模式不再满足:

rust
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);

while let Some(top) = stack.pop() {
    println!("{top}");
}
// 输出:3 2 1

while let 特别适合迭代器和 channel 接收端——持续处理直到信号结束。

可驳斥性:不可驳斥模式 vs 可驳斥模式

Rust 将模式分为两类:

  • 不可驳斥模式(irrefutable):总能匹配成功——用于 let、函数参数、for 循环
  • 可驳斥模式(refutable):可能匹配失败——用于 matchif letwhile let
rust
// 编译通过:Some(x) 是不可驳斥的
let Some(x) = Some(5);  // 错误!let 要求不可驳斥模式
// error[E0005]: refutable pattern in local binding

// 正确:if let 接受可驳斥模式
if let Some(x) = Some(5) {
    println!("{x}");
}

反之亦然——match 中不可驳斥模式会产生警告(因为有一个分支永远不会到达):

rust
match value {
    x => println!("{x}"),   // 警告:不可驳斥模式
    _ => println!("other"), // 警告:不可达分支
}

枚举的内存布局与优化

Rust 编译器对枚举做了精细的布局优化。以 Option<Box<i32>> 为例:

rust
// Box<i32> 是非空指针,None 可以用空指针表示
// 因此 Option<Box<i32>> 的大小 = Box<i32> 的大小 = 1 个指针
// 没有额外的标记字节!

编译器利用“禁止值”(niche)优化:如果某个类型有不可能出现的位模式,枚举可以用它们表示其他变体。例如:

  • 引用永远不为空 → Option<&T> 用空指针表示 None,大小等于 &T
  • bool 只有 falsetrueOption<bool> 用剩余 254 个值之一表示 None
  • char 有大量无效值 → 优化空间很大

这种优化使得 Option<T>Result<T, E> 在很多情况下是零开销抽象

实战:二叉搜索树

综合运用枚举、模式匹配和所有权:

rust
#[derive(Debug)]
enum BinaryTree<T: Ord> {
    Empty,
    NonEmpty(Box<TreeNode<T>>),
}

#[derive(Debug)]
struct TreeNode<T: Ord> {
    element: T,
    left: BinaryTree<T>,
    right: BinaryTree<T>,
}

impl<T: Ord> BinaryTree<T> {
    fn new() -> Self {
        BinaryTree::Empty
    }

    fn add(&mut self, value: T) {
        match self {
            BinaryTree::Empty => {
                *self = BinaryTree::NonEmpty(Box::new(TreeNode {
                    element: value,
                    left: BinaryTree::Empty,
                    right: BinaryTree::Empty,
                }));
            }
            BinaryTree::NonEmpty(ref mut node) => {
                if value <= node.element {
                    node.left.add(value);
                } else {
                    node.right.add(value);
                }
            }
        }
    }

    fn contains(&self, value: &T) -> bool {
        match self {
            BinaryTree::Empty => false,
            BinaryTree::NonEmpty(ref node) => {
                if *value == node.element {
                    true
                } else if *value < node.element {
                    node.left.contains(value)
                } else {
                    node.right.contains(value)
                }
            }
        }
    }
}

这种递归数据结构在 C 中需要手动管理指针和释放,在 Java 中依赖 GC——Rust 用 Box(堆分配)和所有权系统保证了内存安全,同时保持了清晰的所有权语义。

与其它语言的对比

概念RustC/C++JavaHaskell
枚举代数数据类型,可带数据整数常量类 + 枚举值(Java 5+)代数数据类型
OptionOption<T>指针可能为 NULL引用可能为 nullMaybe
ResultResult<T, E>错误码/errno异常Either
模式匹配match(穷举)switch(仅整数)switch(有限类型)case(穷举)
空安全编译时保证(无 null)无(Optional 可选)编译时保证

小结

  • 枚举是代数数据类型:每个变体可携带不同类型的数据。Option<T> 替代 null,Result<T, E> 替代异常。
  • match 是穷举的:编译器强制覆盖所有变体。通配符 _| 简化多分支匹配。匹配守卫 if 添加额外条件,@ 同时解构和绑定。
  • 模式语法丰富:支持字面量、变量绑定、解构结构体和枚举、范围 ..=、引用和切片模式。
  • if letwhile let 简化只关心单一变体的匹配,接受可驳斥模式。
  • 可驳斥性区分模式匹配场景:let 和函数参数要求不可驳斥模式,matchif let 接受可驳斥模式。
  • 零开销抽象:Rust 编译器通过“禁止值”优化,使得 Option<Box<T>>Box<T> 大小相同——没有运行时开销。