rust 学习(10):枚举与模式匹配

文章目录

返回首页 | 上篇 | 下篇


深入理解枚举(enum)与强大的模式匹配 ------ Rust 中没有 null 的秘密


一、回顾与导入

上一篇文章我们学习了结构体,它可以将多个数据组合成一个类型。

但有些值可能是多种可能状态中的一种,比如:

  • 一个颜色可以是 RGB 或 HSV 格式
  • 一个网络请求可能成功返回数据,也可能失败
  • 一个值可能存在,也可能不存在

这时候就需要 枚举(enum) 了。

💡 枚举是 Rust 中最强大的类型之一,结合 match 模式匹配,可以写出非常安全、语义清晰的代码。


二、定义枚举

2.1 简单枚举

rust 复制代码
enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(four);
    route(six);
}

fn route(ip_kind: IpAddrKind) {
    match ip_kind {
        IpAddrKind::V4 => println!("IPv4 地址"),
        IpAddrKind::V6 => println!("IPv6 地址"),
    }
}

2.2 带数据的枚举

Rust 的枚举可以像结构体一样携带数据,每个变体可以携带不同类型和数量的数据:

rust 复制代码
// 标准库中的 IpAddr 就是这样定义的
enum IpAddr {
    V4(u8, u8, u8, u8),   // 元组形式
    V6(String),            // 不同变体可以带不同类型
}

fn main() {
    let home = IpAddr::V4(127, 0, 0, 1);
    let loopback = IpAddr::V6(String::from("::1"));
}

也可以使用结构体作为变体的数据:

rust 复制代码
struct Ipv4Addr {
    a: u8, b: u8, c: u8, d: u8,
}

struct Ipv6Addr {
    addr: String,
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}

三、经典枚举示例:Message

rust 复制代码
enum Message {
    Quit,                       // 不带数据
    Move { x: i32, y: i32 },    // 匿名结构体
    Write(String),              // 单个 String
    ChangeColor(u8, u8, u8),    // 三个 u8
}

impl Message {
    fn call(&self) {
        match self {
            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),
        }
    }
}

fn main() {
    let msg = Message::Write(String::from("Hello, Rust!"));
    msg.call();

    let msg2 = Message::Move { x: 10, y: 20 };
    msg2.call();
}

四、Option 枚举 ------ 替代 Null

Rust 没有 null,取而代之的是 Option<T> 枚举:

rust 复制代码
enum Option<T> {
    None,   // 没有值
    Some(T), // 包含一个类型为 T 的值
}

使用 Option

rust 复制代码
fn main() {
    let some_number = Some(5);
    let some_char = Some('a');
    let absent_number: Option<i32> = None;  // 必须显式标注类型

    // Option<T> 和 T 是不同的类型,不能直接操作
    let x: i32 = 5;
    let y: Option<i32> = Some(5);

    // let sum = x + y;  // ❌ 编译错误!不能把 i32 和 Option<i32> 相加
}

为什么这是好的设计?

必须处理 None 的情况,否则编译器会阻止你:

rust 复制代码
fn main() {
    let maybe_num: Option<i32> = Some(42);

    // 必须显式处理 None 才能获取内部的值
    match maybe_num {
        Some(num) => println!("数字是: {}", num),
        None => println!("没有数字"),
    }
}

这彻底消除了空指针异常这个困扰程序员几十年的问题。


五、match 模式匹配

match 是 Rust 中极其强大的控制流运算符,它将一个值与多个模式进行比较。

5.1 基础语法

rust 复制代码
enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("幸运的便士!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

5.2 匹配带数据的枚举

rust 复制代码
enum Coin {
    Quarter(String),  // 带一个字符串,表示州名
    Other,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Quarter(state) => {
            println!("来自 {} 州的 25 分硬币!", state);
            25
        }
        Coin::Other => 0,
    }
}

fn main() {
    let coin = Coin::Quarter(String::from("California"));
    let value = value_in_cents(coin);
    println!("价值: {} 美分", value);
}

5.3 匹配 Option

rust 复制代码
fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

fn main() {
    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);

    println!("{:?}, {:?}", six, none);
}

5.4 match 必须穷尽所有可能

Rust 要求 match 必须覆盖所有可能的情况

rust 复制代码
fn main() {
    let num = Some(5);

    // ❌ 编译错误:没有处理 None 的情况
    // match num {
    //     Some(5) => println!("five"),
    // }

    // ✅ 正确:使用通配符覆盖其他情况
    match num {
        Some(5) => println!("five"),
        _ => println!("other"),
    }
}

六、通配符与占位符

6.1 使用 _ 通配符

rust 复制代码
fn main() {
    let dice_roll = 9;

    match dice_roll {
        3 => println!("add fancy hat"),
        7 => println!("remove fancy hat"),
        _ => println!("reroll"),  // 其他所有值都走这个分支
    }
}

6.2 使用 _ 忽略不需要的值

rust 复制代码
let some_value = Some(42);

match some_value {
    Some(_) => println!("有值,但我不关心它是什么"),
    None => println!("没有值"),
}

6.3 使用 _other 但保留所有权

_ 会忽略值,如果需要在分支中使用但不想绑定,可以用 _var

rust 复制代码
match maybe_value {
    Some(_x) => {
        // _x 被绑定,但编译器不会警告未使用
    }
    None => {}
}

七、if let 简洁语法

当你只关心 match 中的一个分支,其他分支想忽略时,if let 更简洁:

rust 复制代码
fn main() {
    let config_max = Some(3u8);

    // 用 match 的写法(冗长)
    match config_max {
        Some(max) => println!("最大值是 {}", max),
        _ => (),
    }

    // 用 if let 的写法(简洁)
    if let Some(max) = config_max {
        println!("最大值是 {}", max);
    }
}

if let + else

rust 复制代码
enum Coin {
    Quarter(String),
    Other,
}

fn main() {
    let coin = Coin::Quarter(String::from("Texas"));

    if let Coin::Quarter(state) = coin {
        println!("德州 25 分硬币!");
    } else {
        println!("不是 25 分硬币");
    }
}

八、完整代码示例

rust 复制代码
// 定义一个表示 Web 事件的枚举
#[derive(Debug)]
enum WebEvent {
    PageLoad,                    // 页面加载
    KeyPress(char),              // 按键
    Click { x: i64, y: i64 },    // 点击(带坐标)
}

impl WebEvent {
    fn log(&self) {
        match self {
            WebEvent::PageLoad => println!("页面加载事件"),
            WebEvent::KeyPress(c) => println!("按键事件: '{}'", c),
            WebEvent::Click { x, y } => println!("点击事件: 坐标({}, {})", x, y),
        }
    }
}

// 计算器操作枚举
enum Operation {
    Add,
    Subtract,
    Multiply,
    Divide,
}

fn calculate(a: f64, b: f64, op: Operation) -> Option<f64> {
    match op {
        Operation::Add => Some(a + b),
        Operation::Subtract => Some(a - b),
        Operation::Multiply => Some(a * b),
        Operation::Divide => {
            if b == 0.0 {
                None  // 除以 0 返回 None
            } else {
                Some(a / b)
            }
        }
    }
}

fn main() {
    println!("=== 1. Web 事件处理 ===");
    let events = vec![
        WebEvent::PageLoad,
        WebEvent::KeyPress('a'),
        WebEvent::Click { x: 100, y: 200 },
    ];

    for event in &events {
        event.log();
    }

    println!("\n=== 2. Option 示例 ===");
    let nums = vec![Some(1), Some(2), None, Some(4)];
    let mut sum = 0;

    for num in nums {
        match num {
            Some(n) => sum += n,
            None => println!("遇到 None,跳过"),
        }
    }
    println!("总和: {}", sum);

    println!("\n=== 3. 计算器示例 ===");
    let result = calculate(10.0, 5.0, Operation::Divide);
    match result {
        Some(v) => println!("10 / 5 = {}", v),
        None => println!("除法错误"),
    }

    let result = calculate(10.0, 0.0, Operation::Divide);
    if let Some(v) = result {
        println!("10 / 0 = {}", v);
    } else {
        println!("10 / 0 是未定义的");
    }

    println!("\n=== 4. if let 链 ===");
    let maybe_char = Some('x');

    // 多重条件
    if let Some('x') = maybe_char {
        println!("字符是 x");
    } else if let Some('y') = maybe_char {
        println!("字符是 y");
    } else {
        println!("不是 x 也不是 y");
    }
}

九、常见陷阱与最佳实践

陷阱 1:match 忘记处理所有情况

rust 复制代码
enum Color {
    Red, Green, Blue
}

fn describe(c: Color) -> &'static str {
    match c {
        Color::Red => "红色",
        Color::Green => "绿色",
        // ❌ 忘记处理 Blue -> 编译错误
    }
}

陷阱 2:Option 直接使用

rust 复制代码
fn main() {
    let opt: Option<i32> = Some(42);

    // ❌ 不能直接使用
    // let val = opt + 1;

    // ✅ 需要解包
    match opt {
        Some(v) => println!("{}", v + 1),
        None => println!("无值"),
    }
}

最佳实践

场景 推荐做法
需要处理多个分支 使用 match
只关心一个分支 使用 if let
需要穷尽检查 使用 match + _
表示可能存在/不存在的值 使用 Option<T>
表示可能出错的结果 使用 Result<T, E>(下一章)

十、总结与速查

概念 语法 说明
定义枚举 enum Name { Variant } 定义一组可能的变体
带数据枚举 Variant(T1, T2)Variant { fields } 每个变体可携带数据
Option Option<T> = Some(T)None Rust 中没有 null
match match value { pattern => expr } 模式匹配,必须穷尽
if let if let pattern = value { expr } 只匹配一个模式的简洁语法
通配符 _ 匹配所有未列出的情况

核心要点

  • ✅ 枚举是 Rust 的代数数据类型(ADT),非常强大
  • Option<T> 替代了 null,强制安全处理缺失值
  • match 必须穷尽所有情况,防止遗漏
  • if letmatch 的语法糖,用于单模式匹配
  • ✅ 枚举和 match 结合是 Rust 中最常见的模式之一

十一、思考题

  1. 以下代码会输出什么?
rust 复制代码
fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(5) => println!("匹配到 5"),
        Some(50) => println!("匹配到 50"),
        _ => println!("其他"),
    }
}
  1. 为什么 Rust 选择用 Option<T> 而不是 null

  2. if letmatch 分别应该在什么场景下使用?


参考链接

相关推荐
铅笔侠_小龙虾9 小时前
rust 学习(9):结构体
开发语言·学习·rust
铅笔侠_小龙虾12 小时前
rust 学习(12):常用集合(上)-- Vec
rust
落知秋1 天前
RUST中的trait是什么?
笔记·rust
Embedded-Xin2 天前
中间件—zenoh零基础入门
linux·中间件·rust·机器人·自动驾驶·嵌入式
程序员爱钓鱼2 天前
Rust 可变借用详解:&mut T 与安全修改数据
后端·面试·rust
SomeB1oody2 天前
【RustyML入门】2.9. MeanShift
开发语言·后端·机器学习·rust·教程
SomeB1oody3 天前
【RustyML入门】2.10. 主成分分析
开发语言·后端·机器学习·rust·教程
yushikong3 天前
关于rust开发ch32x033f8p6的一些记录
开发语言·后端·rust