复合类型与模式匹配 beginner 30 分钟 更新 2026-06-15

枚举与 Option

用带数据的枚举建模多态状态,用 Option 表达可能为空。

枚举与 Option

你会学到什么

  • 枚举的每个变体可以携带不同的数据(结构体式字段或元组式字段)。
  • 给枚举写 impl 方法,在方法里 match self
  • match 对枚举做穷尽处理。
  • Option<T> 是标准库枚举,替代其他语言的 null
  • Option 的常用组合子 map / and_then / unwrap_or / unwrap_or_else,以及 if letlet ... else

最小示例

enum Shape {
    Circle { radius: f64 },        // 结构体式变体
    Rectangle { width: f64, height: f64 },
    Triangle(f64, f64),            // 元组式变体
}

示例里还有一个更贴近业务的 Event 枚举(点击、按键、粘贴、关闭),演示枚举如何统一表达“一组互斥的消息”。

运行代码

cd examples
cargo run -p rt_11_enums_option
cargo test -p rt_11_enums_option

代码讲解

枚举 + impl 方法

把行为挂在枚举上:areaShape 的方法,内部 match selfmatch 必须覆盖所有变体,新增 Triangle 后若忘了补分支,编译器会直接报错——这正是枚举安全的来源。name 方法用 .. 忽略不关心的字段,只判断是哪个变体。Event 枚举的 describe 同理,把四种事件渲染成一行日志,展示枚举如何统一表达“一组互斥的消息”。

Option 与组合子

Option<T> 只有两个变体:Some(T)None。Rust 没有 null,“可能没有值”被编码进类型,迫使你显式处理。除了最完整的 match,示例里还演示了几种更简洁的写法:

  • if let Some(n) = ...:只关心 Some 分支时的简写。
  • let Some(first) = ... else { return; }:拿不到值就提前退出,让主流程保持平铺、不嵌套。
  • map:在 Some 内部做变换,None 原样传递(first_even_squared_or_zeromap(|n| n * n))。
  • and_then:上一步是 Some 才继续,否则短路(parse_positiveparse().ok()and_then 过滤正数)。
  • unwrap_or:取值或给一个固定默认值。
  • unwrap_or_else:默认值需要计算时用闭包,只有 None 时才执行,避免无谓开销。

常见错误

Option 直接 unwrap() 而不处理 None

let value = first_even(&[1, 3, 5]).unwrap(); // ❌ None 时 panic

更安全的做法是 matchif letlet ... else,或带默认值的 unwrap_or / unwrap_or_else

练习

  • Shape 再加一个 Square { side } 变体,并补全 areaname
  • Event 加一个 Scroll { delta: i32 } 变体,并补全 describe
  • 写一个返回 Option<&str> 的函数取出切片首元素,再用 map 把它转成大写。

小结

枚举建模“多选一”的数据,match 保证穷尽处理,Option 用类型消灭空指针。

下一步

match 的能力远不止区分变体。下一章系统学习模式匹配。

完整示例代码

下面是 examples/11_enums_option/ 的完整源码。无需 clone 仓库,直接在页面上阅读、复制、对照运行。

examples/11_enums_option/src/main.rs
//! 枚举与 Option:用带数据的枚举建模“多选一”,用 `Option<T>` 表达“可能没有值”。
//!
//! 本示例覆盖:
//! - 带数据的变体(结构体式与元组式)。
//! - 为枚举实现 `impl` 方法,并在方法里 `match self`。
//! - `Option<T>` 的常见组合子:`map` / `and_then` / `unwrap_or` / `unwrap_or_else`。
//! - `if let` 与 `let ... else` 两种简写。

/// 几何图形:每个变体携带各自需要的字段。
#[derive(Debug, PartialEq)]
enum Shape {
    /// 圆,由半径决定。
    Circle { radius: f64 },
    /// 矩形,由宽高决定。
    Rectangle { width: f64, height: f64 },
    /// 三角形(元组式变体):底、高。
    Triangle(f64, f64),
}

impl Shape {
    /// 计算面积——方法内部 `match self`,编译器保证覆盖所有变体。
    fn area(&self) -> f64 {
        match self {
            Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
            Shape::Rectangle { width, height } => width * height,
            Shape::Triangle(base, height) => 0.5 * base * height,
        }
    }

    /// 返回变体的中文名字,演示对 `self` 的只读匹配。
    fn name(&self) -> &'static str {
        match self {
            Shape::Circle { .. } => "圆",
            Shape::Rectangle { .. } => "矩形",
            Shape::Triangle(..) => "三角形",
        }
    }
}

/// 一个更贴近业务的枚举:用户界面事件。
#[derive(Debug)]
enum Event {
    /// 点击坐标。
    Click { x: i32, y: i32 },
    /// 按下某个键。
    KeyPress(char),
    /// 粘贴一段文本。
    Paste(String),
    /// 窗口关闭,无附加数据。
    Close,
}

impl Event {
    /// 把事件渲染成一行人类可读的日志。
    fn describe(&self) -> String {
        match self {
            Event::Click { x, y } => format!("在 ({x}, {y}) 点击"),
            Event::KeyPress(c) => format!("按下按键 '{c}'"),
            Event::Paste(text) => format!("粘贴了 {} 个字符", text.chars().count()),
            Event::Close => "关闭窗口".to_string(),
        }
    }
}

/// 用 `Option` 表达“可能没有结果”:找不到偶数时返回 `None`。
fn first_even(values: &[i32]) -> Option<i32> {
    values.iter().copied().find(|value| value % 2 == 0)
}

/// 把字符串解析成正数:解析失败或非正数都返回 `None`。
///
/// 演示 `and_then`:只有上一步是 `Some` 时才继续,否则短路为 `None`。
fn parse_positive(text: &str) -> Option<i32> {
    text.parse::<i32>()
        .ok()
        .and_then(|n| if n > 0 { Some(n) } else { None })
}

/// 取首个偶数的平方;没有偶数则回退到 0。
///
/// 演示 `map`(在 `Some` 内部变换)与 `unwrap_or`(提供默认值)。
fn first_even_squared_or_zero(values: &[i32]) -> i32 {
    first_even(values).map(|n| n * n).unwrap_or(0)
}

fn main() {
    println!("== 枚举 + impl 方法 ==");
    let shapes = [
        Shape::Circle { radius: 1.0 },
        Shape::Rectangle {
            width: 2.0,
            height: 3.0,
        },
        Shape::Triangle(4.0, 3.0),
    ];
    for shape in &shapes {
        println!("{} 面积 = {:.2}", shape.name(), shape.area());
    }

    println!("\n== 业务枚举 Event ==");
    let events = [
        Event::Click { x: 10, y: 20 },
        Event::KeyPress('A'),
        Event::Paste("你好,世界".to_string()),
        Event::Close,
    ];
    for event in &events {
        println!("{}", event.describe());
    }

    println!("\n== Option 与组合子 ==");
    // match:最完整的处理方式。
    match first_even(&[1, 3, 4, 7]) {
        Some(value) => println!("首个偶数 = {value}"),
        None => println!("没有偶数"),
    }

    // if let:只关心 Some 分支时的简写。
    if let Some(n) = parse_positive("42") {
        println!("解析出正数 = {n}");
    }

    // let ... else:拿不到值就提前返回/退出,主流程保持平铺。
    let Some(first) = first_even(&[5, 6, 7]) else {
        println!("不应发生:切片里没有偶数");
        return;
    };
    println!("let-else 取到首个偶数 = {first}");

    // map / unwrap_or:变换后给默认值。
    println!(
        "首个偶数的平方(无则0) = {}",
        first_even_squared_or_zero(&[1, 3, 5])
    );

    // unwrap_or_else:默认值需要计算时用闭包,避免无谓开销。
    let parsed = parse_positive("oops").unwrap_or_else(|| {
        println!("解析失败,使用兜底值");
        -1
    });
    println!("最终值 = {parsed}");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn rectangle_area() {
        let shape = Shape::Rectangle {
            width: 2.0,
            height: 3.0,
        };
        assert_eq!(shape.area(), 6.0);
    }

    #[test]
    fn circle_area_is_positive() {
        let shape = Shape::Circle { radius: 1.0 };
        assert!((shape.area() - std::f64::consts::PI).abs() < 1e-9);
    }

    #[test]
    fn triangle_area_and_name() {
        let shape = Shape::Triangle(4.0, 3.0);
        assert_eq!(shape.area(), 6.0);
        assert_eq!(shape.name(), "三角形");
    }

    #[test]
    fn finds_first_even() {
        assert_eq!(first_even(&[1, 3, 4, 7]), Some(4));
        assert_eq!(first_even(&[1, 3, 5]), None);
    }

    #[test]
    fn parse_positive_filters_non_positive() {
        assert_eq!(parse_positive("7"), Some(7));
        assert_eq!(parse_positive("0"), None);
        assert_eq!(parse_positive("-3"), None);
        assert_eq!(parse_positive("abc"), None);
    }

    #[test]
    fn squared_or_zero_falls_back() {
        assert_eq!(first_even_squared_or_zero(&[1, 3, 4]), 16);
        assert_eq!(first_even_squared_or_zero(&[1, 3, 5]), 0);
    }

    #[test]
    fn event_describe_counts_chars() {
        let event = Event::Paste("你好".to_string());
        assert_eq!(event.describe(), "粘贴了 2 个字符");
    }
}
examples/11_enums_option/Cargo.toml
[package]
name = "rt_11_enums_option"
version.workspace = true
edition.workspace = true
publish.workspace = true