10. Enums & Patterns — 枚举与模式匹配
如果说结构体是“把几个值放在一起(and)”,那枚举就是“从几个可能性中选一个(or)”。Rust 的枚举是代数数据类型(Algebraic Data Type)的实现——每个变体可以携带不同类型和数量的数据。配合模式匹配,枚举构成了 Rust 中最强大的控制流工具。
计算机科学大师 Donald Knuth 曾写道:“处理非均匀结构是计算机科学的关键特征,而数学家则寻找统一的公理。”枚举正是 Rust 处理“非均匀结构”的核心机制。
枚举的定义
枚举声明列举出该类型可能的所有取值——每个取值称为一个变体(variant):
enum WebEvent {
PageLoad, // 单元变体
KeyPress(char), // 元组变体
Click { x: i64, y: i64 }, // 结构体变体
Paste(String), // 元组变体
}枚举的值一定是刚好一个变体——不可能同时是 PageLoad 又是 Click。这是枚举与结构体最本质的区别:结构体同时拥有所有字段,枚举只持有当前变体的数据。
枚举的内存布局
在内存中,枚举需要足够大的空间容纳最大的变体,外加一个标记(tag)区分当前是哪个变体:
enum RoughTime {
InThePast(WebEvent, String),
JustNow,
InTheFuture(WebEvent, String),
}
// 内存布局(示意):
// [tag: 8字节][WebEvent: 最大变体大小][String: 24字节]Rust 对枚举的优化非常激进——在多种情况下,编译器可以利用“禁止值”来省略标记字节。例如 Option<Box<T>> 用 None 时的空指针来表示,不需要额外的标记。
创建和使用枚举值
// 创建枚举值
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 标准库中最常用的枚举——它表达了“要么有值,要么没有”的语义:
// 标准库中的定义
enum Option<T> {
None,
Some(T),
}Option<T> 与 T 是不同的类型——你不能在期望 T 的地方直接传 Option<T>,反之亦然。这个设计消除了空指针问题——Tony Hoare 称空指针是他“十亿美元的错误”,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 -> T | 同 unwrap,但带自定义 panic 消息 |
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”:
// 标准库中的定义
enum Result<T, E> {
Ok(T),
Err(E),
}与 Option 不同,Result 的失败分支携带了“为什么失败”的信息:
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 中最重要的控制流结构——它穷举所有可能情况,不遗漏任何一个:
match value {
pattern1 => expression1,
pattern2 => expression2,
pattern3 => { // 大括号支持多条语句
statement1;
statement2;
expression3
}
}穷举性(Exhaustiveness)
编译器强制 match 覆盖所有可能的变体:
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 表达式。
通配符和 _ 占位符
不需要使用匹配值时,用 _ 忽略:
fn is_nickel_or_dime(coin: Coin) -> bool {
match coin {
Coin::Nickel | Coin::Dime => true, // | 表示"或"
_ => false, // 匹配所有其他情况
}
}_ 不绑定值——如果匹配了多个 _ 分支,编译器无法确定用哪个值。需要绑定但忽略名称时用变量:
match something {
Some(value) => process(value),
other => { // other 绑定了匹配的值
println!("未处理的值:{other:?}");
default_value
}
}模式语法大全
Rust 的模式匹配语法极为丰富,下面逐一介绍。
字面量模式
match x {
0 => "零",
1 => "一",
2 | 3 => "二或三", // | 匹配多个值
_ => "其他",
}变量绑定
match opt {
Some(v) => println!("值为:{v}"), // v 绑定 Some 内的值
None => println!("无值"),
}解构结构体
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”。
解构枚举
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 条件可以进一步过滤模式:
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!("其他位置"),
}@ 绑定
@ 让你在解构的同时绑定整个值:
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}"),
}范围模式
..= 匹配一个闭区间(包含两端):
match x {
0..=5 => println!("0-5"),
6..=10 => println!("6-10"),
_ => println!("其他"),
}
// 对字符也有效
match ch {
'a'..='j' => println!("早期 ASCII 字母"),
'k'..='z' => println!("后期 ASCII 字母"),
_ => println!("不是小写字母"),
}引用模式
ref 和 ref mut 在匹配时借用而非移动:
match value {
ref r => println!("借用引用:{r:?}"), // r: &T
}
match value {
ref mut r => *r += 1, // r: &mut T
}现代 Rust 中,ref 的使用已经减少——更多使用 & 模式匹配引用。
切片模式
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 let 比 match 更简洁:
// 使用 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 持续匹配直到模式不再满足:
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(top) = stack.pop() {
println!("{top}");
}
// 输出:3 2 1while let 特别适合迭代器和 channel 接收端——持续处理直到信号结束。
可驳斥性:不可驳斥模式 vs 可驳斥模式
Rust 将模式分为两类:
- 不可驳斥模式(irrefutable):总能匹配成功——用于
let、函数参数、for循环 - 可驳斥模式(refutable):可能匹配失败——用于
match、if let、while let
// 编译通过: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 中不可驳斥模式会产生警告(因为有一个分支永远不会到达):
match value {
x => println!("{x}"), // 警告:不可驳斥模式
_ => println!("other"), // 警告:不可达分支
}枚举的内存布局与优化
Rust 编译器对枚举做了精细的布局优化。以 Option<Box<i32>> 为例:
// Box<i32> 是非空指针,None 可以用空指针表示
// 因此 Option<Box<i32>> 的大小 = Box<i32> 的大小 = 1 个指针
// 没有额外的标记字节!编译器利用“禁止值”(niche)优化:如果某个类型有不可能出现的位模式,枚举可以用它们表示其他变体。例如:
- 引用永远不为空 →
Option<&T>用空指针表示None,大小等于&T bool只有false和true→Option<bool>用剩余 254 个值之一表示Nonechar有大量无效值 → 优化空间很大
这种优化使得 Option<T> 和 Result<T, E> 在很多情况下是零开销抽象。
实战:二叉搜索树
综合运用枚举、模式匹配和所有权:
#[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(堆分配)和所有权系统保证了内存安全,同时保持了清晰的所有权语义。
与其它语言的对比
| 概念 | Rust | C/C++ | Java | Haskell |
|---|---|---|---|---|
| 枚举 | 代数数据类型,可带数据 | 整数常量 | 类 + 枚举值(Java 5+) | 代数数据类型 |
| Option | Option<T> | 指针可能为 NULL | 引用可能为 null | Maybe |
| Result | Result<T, E> | 错误码/errno | 异常 | Either |
| 模式匹配 | match(穷举) | switch(仅整数) | switch(有限类型) | case(穷举) |
| 空安全 | 编译时保证(无 null) | 无 | 无(Optional 可选) | 编译时保证 |
小结
- 枚举是代数数据类型:每个变体可携带不同类型的数据。
Option<T>替代 null,Result<T, E>替代异常。 match是穷举的:编译器强制覆盖所有变体。通配符_和|简化多分支匹配。匹配守卫if添加额外条件,@同时解构和绑定。- 模式语法丰富:支持字面量、变量绑定、解构结构体和枚举、范围
..=、引用和切片模式。 if let和while let简化只关心单一变体的匹配,接受可驳斥模式。- 可驳斥性区分模式匹配场景:
let和函数参数要求不可驳斥模式,match和if let接受可驳斥模式。 - 零开销抽象:Rust 编译器通过“禁止值”优化,使得
Option<Box<T>>和Box<T>大小相同——没有运行时开销。