Skip to content

枚举与模式匹配

用枚举精确建模「多种可能」,再用 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(不会造成未定义行为),它是安全的:会在 Nonepanic,也就是「可控的程序终止 + 带回溯的报错」。但在库代码、服务端、任何输入来自用户的 路径上都应避免;改用 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")
}
组合子签名要点作用与邻居的区别
mapFnOnce(T) -> USome(v)Some(f(v))闭包返回普通值,不会产生嵌套 Option
and_thenFnOnce(T) -> Option<U>链式「可能失败」的步骤返回 Option,避免 Option<Option<U>>
unwrap_orT空时给默认值参数立即求值
unwrap_or_elseFnOnce() -> T空时用闭包算默认值惰性,默认值很贵时用它
unwrap_or_default空时用 T::default()要求 T: Default
filterFnOnce(&T) -> bool谓词不满足则变 None保留原值,不转换
take&mut self取出内容,自身置为 None需要 let mut;常用于「搬出但留下合法值」
replace&mut self, T换入新值,返回旧值take 互补
as_ref&selfOption<T>Option<&T>避免 move,想看内容但不想消耗
as_mut&mut selfOption<T>Option<&mut T>原地修改
as_deref&selfOption<T>Option<&T::Target>Option<String>Option<&str>
ok_orEOption<T>Result<T, E>None 变成你指定的错误
ok_or_elseFnOnce() -> E同上,错误惰性构造构造错误的代价高时用它
map_orU, FnOnce(T) -> U一步完成 map + unwrap_or读起来更紧凑

Result<T, E> 预告

Result<T, E> 同样是普通枚举:enum Result<T, E> { Ok(T), Err(E) }。 区别在于 Option 只表达「有没有」,Result 还能表达「为什么没有」。 ? 在返回 Result 的函数里遇到 Err 会提前返回该错误。 完整的错误处理(? 的转换规则、FromBox<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 与 Java enum、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}");
}

输出:

text
circle = 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"));
}

输出:

text
push 5
3 2 1
Some(8080) None

💡 对照if let 相当于 Kotlin 的 if (x is T) + 智能转换,或 C# 的 x is T tlet 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_draftis_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());   // 归档后也不允许
}

输出:

text
Doc { 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 写的代码里到处是 refcargo 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 状态、错误类别),不要用在业务状态机上。


内容以 rustc 1.98.1 · Rust 2024 edition 为基准