Rust 枚举与模式匹配
如果说所有权是 Rust 的灵魂,那枚举 + match 就是 Rust 最优雅的日常工具。Rust 的枚举远比 C/Java 强大——每个变体可以携带不同类型的数据,配合 match 模式匹配,处理分支逻辑极为简洁安全。这一篇是 Rust 实战的核心,务必掌握。
1. Rust 枚举:不只是常量
在 C/Java 里,枚举本质是"有名字的整数"。Rust 的枚举完全不同——它是代数数据类型(ADT),每个变体可以携带不同类型的数据:
// Rust 的枚举比 C/Java 强大得多
// 每个变体(variant)可以【携带不同类型的数据】
// 这叫"代数数据类型"(ADT)
enum Message {
Quit, // 无数据
Move { x: i32, y: i32 }, // 像结构体(命名字段)
Write(String), // 元组形式(一个 String)
ChangeColor(i32, i32, i32), // 元组形式(三个 i32)
}
fn main() {
// 创建枚举值:用 :: 访问变体
let m1 = Message::Quit;
let m2 = Message::Move { x: 10, y: 20 };
let m3 = Message::Write("hello".to_string());
let m4 = Message::ChangeColor(255, 0, 0);
process(m1);
process(m3);
}
// 用 match 处理每种变体(必须穷尽!)
fn process(msg: Message) {
match msg {
Message::Quit => println!("退出"),
Message::Move { x, y } => println!("移动到 ({}, {})", x, y),
Message::Write(text) => println!("写入: {}", text),
Message::ChangeColor(r, g, b) => println!("颜色 RGB({},{},{})", r, g, b),
}
}这个能力的威力在于:你可以用一个 enum 表达"几种互斥的情况",每种情况有自己的数据结构。比如聊天协议的消息、AST 节点、状态机状态,都用 enum + match 清晰建模。
2. Option:Rust 没有 null
Rust 没有 null(空指针)——这是它的核心设计决定。"Null 引用是十亿美元错误"(Tony Hoare 名言),无数 bug 源于"忘记检查 null"。Rust 用 Option<T> 枚举替代:
// Option<T>:Rust 处理"可能没有值"的方式
// 替代了其他语言的 null / None / nil
enum Option<T> {
Some(T), // 有值,类型 T
None, // 没值
}
fn main() {
let some_number: Option<i32> = Some(42);
let no_number: Option<i32> = None;
// 必须显式处理"没有值"的情况,编译器强制
match some_number {
Some(n) => println!("有值: {}", n),
None => println!("没值"),
}
// 常见用法:从某些操作获取 Option
let arr = [1, 2, 3];
let first = arr.get(0); // Option<&i32> = Some(&1)
let out = arr.get(10); // Option<&i32> = None(不 panic!)
// .get 比 [index] 安全:越界返回 None 而不是 panic
if let Some(v) = out {
println!("拿到: {}", v);
} else {
println!("索引越界");
}
}
// 为什么 Option 比 null 好?
// 1. 类型系统强迫你处理 None 的情况,不会"忘了判空"
// 2. Option<T> 和 T 是不同的类型——你不能直接用 Some(5) 当 i32
// 必须先 unwrap 或 match 出来,这就保证了安全
// "Null 引用是十亿美元错误" —— Tony HoareOption 的设计哲学:
- Option<T> 和 T 是不同类型:你不能直接把
Some(5)当 i32 用,必须先 match 或 unwrap。编译器强迫你处理"没值"的情况。 - 不会"忘记判空":这是其他语言最常见的 bug 类型,Rust 在类型系统层面消灭。
- 无运行时开销:Option 在内存布局上和直接持有 T 一样高效(用特殊值表示 None)。
3. match 模式匹配
match 是处理枚举(以及各种值)的标准方式。它强大在模式(Pattern)的丰富性:
fn main() {
let num = 3;
// ===== match 的各种模式 =====
match num {
// 字面量模式
1 => println!("一"),
// 或模式(多个值匹配同一分支)
2 | 3 | 4 => println!("二三四"),
// 范围模式(闭区间)
5..=9 => println!("五到九"),
// 通配符 _ 匹配剩余所有(必须放最后)
_ => println!("其他"),
}
// match 是表达式,有返回值
let text = match num {
1 => "one",
2 => "two",
_ => "many",
};
println!("{}", text);
// 各分支类型必须一致
// match num {
// 1 => "str",
// _ => 42, // 编译错误!类型不匹配
// }
}几个核心特性:
- 必须穷尽:漏写一个分支编译报错。这是 Rust 比 switch 安全的地方——不会"忘记处理某情况"。
- 是表达式:有返回值,各分支类型要一致。
- 模式丰富:字面量、范围、或、通配符等。
- _ 是通配符:匹配剩余所有情况,放最后。相当于 default。
4. 解构(强大模式)
match 最强大的能力是解构(destructure)——把元组、结构体、枚举的内部数据"拆"出来:
fn main() {
// ===== 解构元组 =====
let point = (3, 5);
match point {
(0, 0) => println!("原点"),
(0, y) => println!("y 轴,y = {}", y),
(x, 0) => println!("x 轴,x = {}", x),
(x, y) => println!("普通点 ({}, {})", x, y),
}
// ===== 解构结构体 =====
struct Point { x: i32, y: i32 }
let p = Point { x: 5, y: -3 };
match p {
Point { x: 0, y } => println!("在 y 轴, y = {}", y),
Point { x, y: 0 } => println!("在 x 轴, x = {}", x),
// 简写:字段名和变量名相同时
Point { x, y } => println!("({}, {})", x, y),
}
// ===== 解构枚举(配合变体数据)=====
enum Shape {
Circle(f64),
Rectangle(f64, f64),
}
let s = Shape::Rectangle(3.0, 4.0);
let area = match s {
Shape::Circle(r) => 3.14 * r * r,
Shape::Rectangle(w, h) => w * h,
};
println!("面积: {}", area);
}解构让代码非常简洁——一步完成"判断 + 取值"。配合枚举,处理分支逻辑极为优雅。这是函数式编程的经典技巧,Rust 把它做到了极致。
5. if let / while let:简写
当你只关心一种情况时,用 if let 比 match 更简洁。它是 match 的语法糖:
fn main() {
let some: Option<i32> = Some(42);
// 当只关心一种情况时,用 if let 比 match 更简洁
// match 写法(啰嗦):
match some {
Some(x) => println!("拿到: {}", x),
_ => (), // 必须处理其他情况
}
// if let 写法(简洁):只关心 Some
if let Some(x) = some {
println!("拿到: {}", x);
} else {
println!("没值"); // else 可选
}
// while let:循环匹配,直到不匹配
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("弹出: {}", top); // 3, 2, 1
}
// 经验:
// - 关心两种以上情况 -> 用 match
// - 只关心一种情况,其他忽略 -> 用 if let
}经验法则:
- 关心两种以上情况 → 用 match(强制穷尽)。
- 只关心一种,其他忽略 → 用 if let(简洁)。
- 循环匹配直到失败 → 用 while let(如遍历栈、读行)。
6. 枚举也能有方法
和结构体一样,枚举也能有 impl 块,定义方法和关联函数:
// 枚举也能有 impl 块(像结构体一样)
enum TrafficLight {
Red,
Yellow,
Green,
}
impl TrafficLight {
fn duration(&self) -> u32 {
match self {
TrafficLight::Red => 30,
TrafficLight::Yellow => 5,
TrafficLight::Green => 45,
}
}
fn next(&self) -> TrafficLight {
match self {
TrafficLight::Red => TrafficLight::Green,
TrafficLight::Green => TrafficLight::Yellow,
TrafficLight::Yellow => TrafficLight::Red,
}
}
}
fn main() {
let light = TrafficLight::Red;
println!("红灯持续 {} 秒", light.duration());
let next = light.next();
println!("下一个是绿灯,持续 {} 秒", next.duration());
}枚举 + impl + match 是 Rust 处理"状态机"的标配。比如红绿灯、订单状态、解析器状态等,都用这种方式建模,代码既清晰又安全(不会进入未定义状态)。
7. 模式匹配的更多技巧
match 的模式还有几个常用技巧:
- 绑定值:用 @ 同时绑定和匹配范围,如
n @ 1..=100 => ...。 - 守卫(guard):在模式后加 if 条件,如匹配 Some(x) 且只有当 x 大于 0 时才执行该分支。
- 嵌套解构:可以解构多层,如把 Option 里的元组一次性拆开。
- 引用模式:在 match 引用时,用
&模式。
这些进阶技巧在标准库和第三方库里随处可见,熟练掌握后,你能写出非常地道的 Rust 代码。
8. 枚举 vs 结构体:何时用哪个?
选择指南:
- 结构体:描述"一个东西"的多个属性。如 User 有 name、age、email。
- 枚举:描述"一个东西"的多种互斥情况。如订单状态可能是"待支付/已支付/已发货/已完成"。
经验:当你发现自己在写"如果 A 类型,这样处理;如果 B 类型,那样处理"时,就该用枚举了。枚举让"可能性"显式化,match 强制处理每种情况。
9. 实战:解析命令
用枚举 + match 建模"用户输入的命令",是 Rust 的经典模式:
- 定义一个
Command枚举,带 Add、List、Delete、Help 等变体(Add 携带字符串,Delete 携带数字)。 - 解析用户输入为 Command 枚举值。
- 用 match 处理每种命令,编译器保证不漏。
- 新增命令时,match 会强制你处理——这是重构的保险。
小结
这一篇你掌握了 Rust 枚举和模式匹配的威力:枚举(变体可携带数据,ADT)、Option<T>(替代 null,强制处理空值)、match(穷尽模式匹配)、解构(拆出内部数据)、if let / while let(简写)。枚举 + match 是 Rust 处理分支逻辑的标配,比 switch + if-null 安全优雅太多。
下一篇我们看 Trait(特征)——Rust 实现抽象和多态的方式,它和"接口"概念相似但有独特之处。
← 上一篇 Rust 结构体
下一篇 Rust Trait 特征 →