枚举与模式匹配
用枚举精确建模「多种可能」,再用
match强制处理每一种情况。
枚举:代数数据类型(ADT)
每个变体可以有完全不同的数据
rust
#[derive(Debug, Clone, PartialEq)]
enum Message {
Quit, // 单元变体:无数据
Move { x: i32, y: i32 }, // 结构体变体:具名字段
Write(String), // 元组变体:位置字段
ChangeColor(u8, u8, u8),
}
impl Message {
fn describe(&self) -> String {
match self {
Message::Quit => "退出".to_string(),
Message::Move { x, y } => format!("移动到 ({x}, {y})"),
Message::Write(s) => format!("写入 {s:?}"),
Message::ChangeColor(r, g, b) => format!("颜色 #{r:02X}{g:02X}{b:02X}"),
}
}
}
fn main() {
let batch = [
Message::Quit,
Message::Move { x: 3, y: -2 },
Message::Write(String::from("hello")),
Message::ChangeColor(255, 128, 0),
];
for m in &batch { println!("{}", m.describe()); }
}💡 对照:这就是 Haskell / F# / OCaml 的「代数数据类型」(algebraic data type, ADT), 也叫「和类型」(sum type)。Rust 的
enum与它们语义一致,也接近 TypeScript 的可辨识联合 (discriminated union),但每个变体可以带不同数量、不同类型的载荷,且大小在编译期固定。
枚举的内存布局:大小等于最大变体
rust
use std::mem::size_of;
enum Small { A, B }
enum WithData { None, Num(i32), Pair(f64, f64), Text(String) }
fn main() {
println!("{}", size_of::<Small>()); // 1:只需区分 2 个变体
println!("{}", size_of::<WithData>()); // 32:(String 是 24 字节) + 判别式 + 对齐填充
println!("{}", size_of::<Option<u8>>()); // 2
println!("{}", size_of::<Option<&u8>>()); // 8:利用了空指针优化(niche optimization)
}🧠 原理:枚举 = 判别式(discriminant,也叫 tag)+ 所有变体里最大那个的载荷。 因此
enum的大小是「最坏情况」。但对于Option<&T>、Option<Box<T>>、Option<NonZeroU32>这类类型,Rust 发现&T永远不可能是 0,就把None编码为 0,不再额外占空间 —— 这叫 niche optimization(空位优化),是Option零开销的原因。
Option<T>:把 null 从语言里彻底删掉
Option<T> 就是标准库里的一个普通枚举,定义只有两行:
rust
// 标准库中的定义(简化)
// pub enum Option<T> { None, Some(T) }rust
fn find_user(id: u64) -> Option<String> {
if id == 1 { Some(String::from("ana")) } else { None }
}
fn main() {
// 1) match:最完整
match find_user(1) {
Some(name) => println!("找到 {name}"),
None => println!("没有这个用户"),
}
// 2) if let:只关心有值的情况
if let Some(name) = find_user(2) {
println!("{name}");
} else {
println!("id=2 不存在");
}
// 3) unwrap / expect:只在“逻辑上不可能为 None”时用,否则 panic
let name = find_user(1).expect("id=1 一定存在"); // expect 会带上你的说明信息
println!("{name}");
// let bad = find_user(2).unwrap(); // panic: called `Option::unwrap()` on a `None` value
}⚠️ 注意:
unwrap()不是unsafe(不会造成未定义行为),它是安全的:会在None时 panic,也就是「可控的程序终止 + 带回溯的报错」。但在库代码、服务端、任何输入来自用户的 路径上都应避免;改用expect("说明为什么这里不该是 None")让 panic 信息可诊断, 或者在能返回错误的地方用?传播(见 错误处理)。
? 用在返回 Option 的函数里,可以在 None 时立即返回 None:
rust
fn first_char_len(s: &str) -> Option<usize> {
let first = s.chars().next()?; // 空字符串时直接返回 None
Some(first.len_utf8())
}
fn main() {
println!("{:?} {:?}", first_char_len("你好"), first_char_len(""));
}输出:
Some(3) None
Option 常用组合子速查
rust
fn main() {
let x: Option<i32> = Some(3);
let n: Option<i32> = None;
println!("{:?}", x.map(|v| v * 2)); // Some(6)
println!("{:?}", x.and_then(|v| if v > 2 { Some(v) } else { None })); // Some(3)
println!("{}", n.unwrap_or(0)); // 0
println!("{}", n.unwrap_or_else(|| 7 + 1)); // 8(惰性求值)
println!("{:?}", x.filter(|v| *v > 10)); // None
let mut gift = Some(String::from("gift"));
let taken = gift.take(); // 取走内容,gift 变成 None
println!("{:?} {:?}", taken, gift); // Some("gift") None
let owned = Some(String::from("hi"));
let r: Option<&String> = owned.as_ref(); // 借用而非 move
println!("{:?}", r.map(|s| s.len())); // Some(2)
let d: Option<&str> = owned.as_deref(); // 借用 + Deref 到 str
println!("{d:?}"); // Some("hi")
println!("{:?}", n.ok_or("missing")); // Err("missing")
}| 组合子 | 签名要点 | 作用 | 与邻居的区别 |
|---|---|---|---|
map | FnOnce(T) -> U | Some(v) → Some(f(v)) | 闭包返回普通值,不会产生嵌套 Option |
and_then | FnOnce(T) -> Option<U> | 链式「可能失败」的步骤 | 返回 Option,避免 Option<Option<U>> |
unwrap_or | T | 空时给默认值 | 参数立即求值 |
unwrap_or_else | FnOnce() -> T | 空时用闭包算默认值 | 惰性,默认值很贵时用它 |
unwrap_or_default | — | 空时用 T::default() | 要求 T: Default |
filter | FnOnce(&T) -> bool | 谓词不满足则变 None | 保留原值,不转换 |
take | &mut self | 取出内容,自身置为 None | 需要 let mut;常用于「搬出但留下合法值」 |
replace | &mut self, T | 换入新值,返回旧值 | 与 take 互补 |
as_ref | &self | Option<T> → Option<&T> | 避免 move,想看内容但不想消耗 |
as_mut | &mut self | Option<T> → Option<&mut T> | 原地修改 |
as_deref | &self | Option<T> → Option<&T::Target> | Option<String> → Option<&str> |
ok_or | E | Option<T> → Result<T, E> | None 变成你指定的错误 |
ok_or_else | FnOnce() -> E | 同上,错误惰性构造 | 构造错误的代价高时用它 |
map_or | U, FnOnce(T) -> U | 一步完成 map + unwrap_or | 读起来更紧凑 |
Result<T, E> 预告
Result<T, E> 同样是普通枚举:enum Result<T, E> { Ok(T), Err(E) }。 区别在于 Option 只表达「有没有」,Result 还能表达「为什么没有」。 ? 在返回 Result 的函数里遇到 Err 会提前返回该错误。 完整的错误处理(? 的转换规则、From、Box<dyn Error>、thiserror/anyhow)见 错误处理。
rust
fn parse_port(s: &str) -> Result<u16, std::num::ParseIntError> {
let n: u16 = s.trim().parse()?; // 失败时自动 return Err(...)
Ok(n)
}
fn main() {
println!("{:?}", parse_port(" 8080 "));
println!("{:?}", parse_port("nope"));
}输出:
Ok(8080) Err(ParseIntError { kind: InvalidDigit })
💡 对照:
enum与 Javaenum、TypeScript 联合类型、C++std::variant在「变体能否带不同数据」「穷尽性检查」「空值处理」等维度上的完整对照表, 见 「与其他语言的对照」。
枚举 + match 实战
计算面积:三种形态的变体一起用
rust
#[derive(Debug, Clone, Copy, PartialEq)]
enum Shape {
Circle(f64),
Rect { w: f64, h: f64 },
Triangle(f64, f64, f64),
}
impl Shape {
fn area(&self) -> f64 {
// *self 是 Copy 的,match *self 让变体里的 f64 直接按值绑定
match *self {
Shape::Circle(r) => std::f64::consts::PI * r * r,
Shape::Rect { w, h } => w * h,
Shape::Triangle(a, b, c) => {
let s = (a + b + c) / 2.0; // 海伦公式
(s * (s - a) * (s - b) * (s - c)).max(0.0).sqrt()
}
}
}
fn name(&self) -> &'static str {
match self {
Shape::Circle(_) => "circle",
Shape::Rect { .. } => "rect", // .. 忽略剩余字段
Shape::Triangle(..) => "triangle", // .. 忽略全部载荷
}
}
}
fn main() {
let all = [
Shape::Circle(1.0),
Shape::Rect { w: 3.0, h: 4.0 },
Shape::Triangle(3.0, 4.0, 5.0),
];
for s in &all { println!("{} = {:.4}", s.name(), s.area()); }
let total: f64 = all.iter().map(|s| s.area()).sum();
println!("total = {total:.4}");
}输出:
textcircle = 3.1416 rect = 12.0000 triangle = 6.0000 total = 21.1416
🧠 原理:
match *self之所以可行,是因为Shape实现了Copy(内部全是f64)。 如果载荷里有String,就必须写match self并用ref/默认绑定模式借出内容, 否则会报cannot move out of *self, which is behind a shared reference。
if let / while let / let else
rust
#[derive(Debug)]
enum Cmd { Push(i32), Pop, Stop }
fn main() {
// if let:只处理一个变体,其余忽略
let c = Cmd::Push(5);
if let Cmd::Push(v) = c { println!("push {v}"); } else { println!("not a push"); }
// while let:反复匹配直到失败(注意:match 的值必须每轮都重新求值)
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() { print!("{top} "); }
println!();
// let else(Rust 1.65+):失败分支必须发散(return/continue/panic),主流程不缩进
fn parse_port(s: &str) -> Option<u16> {
let Ok(n) = s.parse::<u32>() else { return None };
u16::try_from(n).ok()
}
println!("{:?} {:?}", parse_port("8080"), parse_port("70000"));
}输出:
textpush 5 3 2 1 Some(8080) None
💡 对照:
if let相当于 Kotlin 的if (x is T)+ 智能转换,或 C# 的x is T t。let else则是 Go 的if err != nil { return }惯用法在类型层面的对应物 —— 把异常路径提前踢出去,让主逻辑保持在最浅缩进。
重构示例:一堆 bool 标志位 → 枚举
重构前(before) —— 典型的「用多个 bool 表示互斥状态」写法:
rust
#[derive(Debug)]
struct Doc {
is_draft: bool,
is_published: bool, // 与 is_draft 互斥,但类型系统不知道
is_archived: bool, // 三个 bool 有 8 种组合,只有 3 种合法
pinned: bool,
}
impl Doc {
fn new() -> Self {
Doc { is_draft: true, is_published: false, is_archived: false, pinned: false }
}
// 每个方法都得重新检查一堆标志位,漏一个就是运行时 bug
fn publish(&mut self) {
if self.is_draft && !self.is_published && !self.is_archived {
self.is_draft = false;
self.is_published = true;
}
}
fn shows_on_home(&self) -> bool {
self.is_published && !self.is_archived && self.pinned
}
}
fn main() {
let mut d = Doc::new();
d.publish();
// 谁都能写出非法状态,编译期完全无感:
d.is_draft = true;
d.is_archived = true;
println!("{:?} home={}", d, d.shows_on_home());
}问题:非法状态是可表示的(representable)。任何代码任何时刻都能把 is_draft 和 is_archived 同时设为 true,而「草稿已被归档」在业务上没有意义。
重构后(after) —— 状态是枚举,非法状态无法构造:
rust
#[derive(Debug, Clone, Copy, PartialEq)]
enum State { Draft, Published, Archived }
#[derive(Debug)]
struct Doc { state: State, pinned: bool }
impl Doc {
fn new() -> Self { Doc { state: State::Draft, pinned: false } }
// 状态迁移集中在一处;转换失败是类型化的 Result,而不是静默无操作
fn publish(&mut self) -> Result<(), String> {
match self.state {
State::Draft => { self.state = State::Published; Ok(()) }
State::Published => Err("已经发布过了".to_string()),
State::Archived => Err("已归档的文档不能发布".to_string()),
}
}
fn archive(&mut self) { self.state = State::Archived; }
// 判断条件只依赖一个值,不可能出现“两个标志矛盾”的情况
fn shows_on_home(&self) -> bool {
matches!(self.state, State::Published) && self.pinned
}
}
fn main() {
let mut d = Doc::new();
println!("{:?} home={}", d, d.shows_on_home());
println!("{:?}", d.publish());
println!("{:?}", d.publish()); // 第二次发布被明确拒绝
d.pinned = true;
println!("{:?} home={}", d, d.shows_on_home());
d.archive();
println!("{:?}", d.publish()); // 归档后也不允许
}输出:
textDoc { state: Draft, pinned: false } home=false Ok(()) Err("已经发布过了") Doc { state: Published, pinned: true } home=true Err("已归档的文档不能发布")
🧠 原理:这叫「让非法状态不可表示」(make illegal states unrepresentable)。 判据很简单:如果两个
bool永远不该同时为true,它们其实是同一个枚举的两个变体。 字符串状态机(status == "published")同理 —— 拼写错误在运行时才暴露,枚举在编译期就报错。
match 的进阶模式
绑定、|、@、范围、哨兵
rust
#[derive(Debug)]
enum Cmd {
Quit,
Move { dx: i32, dy: i32 },
Say(String),
}
fn run(cmd: Cmd) {
match cmd {
Cmd::Quit => println!("退出"),
// 字面量模式 + 哨兵(guard):dy 只有走到这里才绑定
Cmd::Move { dx: 0, dy: 0 } => println!("原地不动"),
Cmd::Move { dx, dy } if dx.abs() <= 10 && dy.abs() <= 10 => println!("小步移动 {dx},{dy}"),
Cmd::Move { dx, dy } => println!("拒绝越界移动 {dx},{dy}"),
// ref 借出内部 String(cmd 已经被消耗,但这里我们只需要看)
Cmd::Say(ref msg) if msg.starts_with('!') => println!("喊叫 {msg}"),
// 绑定整个值:无需重新拼装
Cmd::Say(msg) => println!("说 {msg}"),
}
}
fn classify(n: i32) -> &'static str {
match n {
0 => "zero",
1 | 2 | 3 => "small", // | 表示“或”
4..=9 => "medium", // 范围(含两端)
x if x < 0 => "negative", // 哨兵 + 绑定
_ => "large", // 兜底
}
}
fn main() {
for n in [-5, 0, 2, 7, 99] { print!("{}:{} ", n, classify(n)); }
println!();
run(Cmd::Move { dx: 3, dy: 4 });
run(Cmd::Say("!hi".into()));
// @ 绑定:既做范围判断,又把值绑出来
let v = 5;
match v { n @ 1..=9 => println!("digit {n}"), _ => println!("other") }
}输出:
-5:negative 0:zero 2:small 7:medium 99:large然后小步移动 3,4/喊叫 !hi/digit 5
嵌套解构与 _ / ..
rust
#[derive(Debug)]
struct Wrapper(Option<Result<Vec<(u8, &'static str)>, String>>);
fn main() {
let ok = Wrapper(Some(Ok(vec![(1, "one"), (2, "two")])));
let err = Wrapper(Some(Err("boom".to_string())));
let none = Wrapper(None);
for w in [&ok, &err, &none] {
match w {
// 嵌套解构:一次把四层结构拆开
Wrapper(Some(Ok(pairs))) if pairs.len() == 2 => {
let (k, v) = pairs[1];
println!("两对,第二对 {k} => {v}");
}
Wrapper(Some(Ok(pairs))) => println!("{} 对", pairs.len()),
Wrapper(Some(Err(msg))) => println!("错误: {msg}"),
Wrapper(None) => println!("空"),
}
}
// _ 忽略单个位置,.. 忽略“剩余的全部”
let triple = (1, 2, 3);
let (first, ..) = triple;
let (_, middle, _) = triple;
println!("{first} {middle}");
let arr = [10, 20, 30, 40];
if let [head, .., last] = arr { println!("{head}..{last}"); }
}输出:
两对,第二对 2 => two/错误: boom/空/1 2/10..40
ref / ref mut 与 2024 edition 的 match ergonomics 变化
背景术语:当你匹配一个引用(&T)而模式本身不是引用模式(如 Some(x))时, Rust 会启用「默认绑定模式」(default binding modes,俗称 match ergonomics / 匹配人体工学): 编译器自动把 x 绑定成 &T 内部字段的引用,而不是报「cannot move out of borrowed content」。
rust
fn main() {
let mut opt = Some(String::from("x"));
// 匹配 &mut Option<String>,模式是 Some(s):s 自动被绑定为 &mut String
if let Some(s) = &mut opt {
s.push('!'); // 可以直接当 &mut String 用
}
println!("{opt:?}"); // Some("x!")
// 只想借出:as_ref 或匹配 &opt
match &opt {
Some(s) => println!("借到 {s}"), // s: &String
None => {}
}
// 显式 ref / ref mut:仍然可用,但 2024 edition 收紧了规则
match &opt {
&Some(ref s) => println!("显式 ref: {s}"),
&None => {}
}
}2024 edition 的变化:当模式匹配的资源已经处于「隐式借用」状态时,再写 ref / ref mut / & 会被拒绝(在 2021 及更早只是警告或允许),必须去掉这些多余的修饰符:
rust
// 下面这段在 edition 2024 下会直接报错
fn main() {
let opt: Option<String> = Some(String::from("x"));
match &opt {
Some(ref s) => println!("{s}"), // 错误:不能显式借用
None => {}
}
}编译器给出的错误(节选,edition 2024):
text
error: cannot explicitly borrow within an implicitly-borrowing pattern
--> src/main.rs:3:14
|
3 | Some(ref s) => println!("{s}"),
| ^^^ explicit `ref` binding modifier not allowed when implicitly borrowing
|
= note: matching on a reference type with a non-reference pattern implicitly borrows the contents
help: remove the unnecessary binding modifier
|
3 - Some(ref s) => println!("{s}"),
3 + Some(s) => println!("{s}"),2024 edition 的第二个变化:& / &mut 模式现在会重置绑定模式, 而不是像 2021 那样「叠加」一层引用。体会这两段的区别:
rust
fn main() {
let mut p = (1, 2);
// 2021: x 的类型是 &mut i32(&mut 的“&mut”叠加了一层)
// 2024: x 的类型是 &mut i32 —— 但整体匹配 (&mut p) 时不再是 &mut &mut i32
match &mut p {
&mut (ref mut x, _) => *x += 10,
}
println!("{p:?}"); // (11, 2)
// 整个值的绑定:2024 下是 &mut (i32, i32),不是 &(i32, i32)
match &mut p {
whole => println!("whole = {whole:?}"),
}
}⚠️ 陷阱:如果你在 2021 edition 写的代码里到处是
ref,cargo fix --edition会自动帮你 处理大部分,但嵌套模式里的情况要人工确认。规则记忆法: 「引用模式(&/&mut开头的模式)会把绑定模式重置为『按值』;非引用模式匹配引用时, 编译器自动为你加一层引用。」 二者不能同时用。
match 穷尽性与 #[non_exhaustive]
match 必须覆盖所有可能,否则 E0004。这让「给枚举加变体」变成一次全仓库的编译期检查:
rust
#[derive(Debug)]
enum Status { Idle, Running, Done }
fn label(s: &Status) -> &'static str {
match s {
Status::Idle => "空闲",
Status::Running => "运行中",
// 在这里删掉 Status::Done 会立刻报 E0004: non-exhaustive patterns: `&Done` not covered
Status::Done => "完成",
}
}
fn main() { println!("{}", label(&Status::Idle)); }对跨 crate 的公开枚举,如果不想让下游用户被「新增变体」打破编译,用 #[non_exhaustive]:
rust
// 在库 crate 里
#[non_exhaustive]
pub enum Status { Idle, Running, Done }rust
// 在下游 crate 里:`Status` 来自库 crate,这里只写用法,所以单独编译会报类型找不到
fn label(s: &Status) -> &'static str {
match s {
Status::Idle => "空闲",
Status::Running => "运行中",
Status::Done => "完成",
_ => "未知状态", // 必需:`Status` is marked as non-exhaustive,
// so a wildcard `_` is necessary to match exhaustively
}
}
fn main() { println!("{}", label(&Status::Idle)); }要点:
#[non_exhaustive]只对其它 crate 生效;定义它的 crate 内部依然要求穷尽匹配。- 加在 struct 上时,作用是禁止其它 crate 用字面量构造该结构体,只能通过你提供的构造函数。
- 代价:下游永远无法用穷尽匹配,你的新增变体会静默落入
_分支 —— 用在「预期会扩展」的 枚举上(如 HTTP 状态、错误类别),不要用在业务状态机上。