找回密码
立即注册
搜索
发回帖 发新帖

6415

积分

0

好友

810

主题
发表于 2 小时前 | 查看: 7| 回复: 0

本篇导读:今天我们学习 Rust 类型系统中最具表达力、也最能体现其安全哲学的特性之一——枚举(enum),以及与之形影不离的 match 模式匹配。

如果你来自 C/C++/Java,可能以为枚举就是“一组起了名字的整数常量”。Rust 的枚举强大得多:每个变体(variant)都可以携带属于自己的、类型不同的数据,这种类型在函数式语言中被称为代数数据类型(ADT)。

本篇你将学会:定义带数据的枚举、标准库最重要的两个枚举 Option<T> 与 Result<T, E>、用 match 穷尽匹配、用 if let / let-else 简化代码。理解了它们,你就理解了 Rust 如何从类型系统层面彻底消灭空指针异常。


一、定义枚举

1. 最简单的枚举

enum IpAddrKind {
    V4,
    V6,
}

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

枚举的变体通过 枚举名::变体名 使用。

2. 变体可以携带数据

Rust 枚举的强大之处在于:每个变体可以独立地携带数据,数据类型还可以各不相同。假设 IP 地址 V4 是四个数字、V6 是一个字符串:

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

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

这比用“结构体 + 枚举”组合要简洁得多。变体甚至可以携带结构体,或者任意复杂的数据:

struct Ipv4Addr { /* ... */ }
struct Ipv6Addr { /* ... */ }

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

3. 一个更丰富的例子:消息类型

enum Message {
    Quit,                       // 不带数据
    Move { x: i32, y: i32 },    // 带具名字段,像结构体
    Write(String),              // 带一个 String
    ChangeColor(i32, i32, i32), // 带三个 i32
}

一个枚举就把四种完全不同形态的消息统一到了同一个 Message 类型下,这在其他语言里往往需要一个公共基类加四个子类才能表达。枚举同样可以用 impl 定义方法:

impl Message {
    fn call(&self) {
        println!("处理了一条消息");
    }
}

let m = Message::Write(String::from("hello"));
m.call();

二、Option<T>:编码“可能没有值”

这是标准库中使用频率最高的枚举,也是 Rust 消灭空指针的关键。

1. 空值的问题

许多语言都有“空值(null / nil / None)”概念,代表“这里没有值”。但它也是无数 bug 的根源:你以为变量里有东西,调用它的方法,结果它是 null,程序当场崩溃——Tony Hoare 称自己发明 null 为“十亿美元错误”。

Rust 中根本没有 null。那如何表示“一个值可能存在、也可能不存在”呢?答案是用 Option<T> 枚举:

enum Option<T> {
    Some(T), // 有值,值的类型是 T
    None,    // 没有值
}

Option<T> 如此常用,以至于它的两个变体被直接放进了 prelude——你可以直接写 Some 和 None,无需前缀:

let some_number = Some(5);
let some_string = Some("一段文字");
let absent_number: Option<i32> = None; // None 时需要标注类型

2. 为什么这样更安全?

关键在于:Option<T> 和 T 不是同一个类型。编译器不允许你把一个“可能为空”的值当作“一定有值”来直接使用:

let x: i8 = 5;
let y: Option<i8> = Some(5);

// let sum = x + y; // 错误:i8 与 Option<i8> 不能直接相加

在你想使用 y 之前,必须先处理“它是 None 怎么办”。空值的隐患从“运行时才爆炸”变成了“编译期就逼你处理”。Rust 不是让空值消失,而是让你无法对空值视而不见。

3. 取出 Option 中的值

Option<T> 提供了大量方法,常用的有:

let x = Some(10);

assert_eq!(x.unwrap_or(0), 10);       // 是 Some 就取值,None 用默认值 0
assert_eq!(x.is_some(), true);        // 是否为 Some
assert_eq!(x.is_none(), false);       // 是否为 None

let y: Option<i32> = None;
assert_eq!(y.unwrap_or(0), 0);

生产代码中应尽量避免裸用 unwrap()(None 时会 panic),优先用 unwrap_or 或下面的 match。


三、match:穷尽式匹配

match 是处理枚举的“标配武器”,第 2 篇我们已经用它比较数字。它的强大在于可以把变体携带的数据直接取出来。

1. 匹配枚举并取出数据

fn route(ip: IpAddr) {
    match ip {
        IpAddr::V4(a, b, c, d) => println!("IPv4 地址:{a}.{b}.{c}.{d}"),
        IpAddr::V6(addr) => println!("IPv6 地址:{addr}"),
    }
}

V4(a, b, c, d) 这个模式不仅匹配了变体,还把它携带的四个值绑定到了变量 a b c d 上。

2. 处理 Option

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(n) => Some(n + 1),
        None => None,
    }
}

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

3. match 必须穷尽

Rust 要求 match 覆盖所有可能,漏掉 None 分支就编译报错:

match x {
    Some(n) => println!("{n}"),
    // 错误:pattern None not covered
}

这保证你永远不会忘记处理“没有值”的情况。

4. 通配符 _

当你不想逐一列出剩余所有情况时,用 _ 作为兜底模式:

let dice = 3;
match dice {
    1 => println!("一点"),
    2 => println!("两点"),
    _ => println!("其他点数"), // 匹配除 1、2 外的任意值
}

_ 会匹配任意值但不绑定数据。如果想在兜底时停止循环等,可以在分支中写多条语句。

5. 匹配是表达式

别忘了 match 是表达式,可以直接用于赋值:

let coin = 5;
let value = match coin {
    1 => 1,
    5 => 5,
    10 => 10,
    _ => 0,
};

四、if let 与 let-else:只关心一种情况时

match 很严谨,但有时你只关心一个变体,其余全部忽略,写一堆 _ => () 很啰嗦。这时用 if let:

let some_value = Some(3);

match some_value {
    Some(3) => println!("是三!"),
    _ => (),
}

// 等价的简洁写法
if let Some(3) = some_value {
    println!("是三!");
}

if let 还可以带 else,相当于 match 里的兜底分支:

let coin: Option<i32> = None;
let mut count = 0;
if let Some(i) = coin {
    println!("硬币面值:{i}");
} else {
    count += 1;
    println!("没有硬币,计数:{count}");
}

let ... else:提前返回

较新的 Rust 提供了 let-else 语法,非常适合“如果不是我期望的模式,就提前退出”:

fn describe(s: Option<&str>) {
    let Some(text) = s else {
        println!("没有内容,直接返回");
        return;
    };
    // 到这里 text 一定是有效的 &str
    println!("内容是:{text}");
}

它让“先处理异常路径、再走主逻辑”的代码格外清晰,是现代 Rust 的推荐写法。


五、综合示例:一个简易计算器的指令解析

我们用枚举 + match 实现一个能解析指令的小系统:

#[derive(Debug)]
enum Op {
    Add(f64, f64),
    Sub(f64, f64),
    Mul(f64, f64),
    Div(f64, f64),
}

impl Op {
    fn calculate(&self) -> Option<f64> {
        match self {
            Op::Add(a, b) => Some(a + b),
            Op::Sub(a, b) => Some(a - b),
            Op::Mul(a, b) => Some(a * b),
            Op::Div(a, b) => {
                if *b == 0.0 {
                    None // 除数为零,返回 None 表示失败
                } else {
                    Some(a / b)
                }
            }
        }
    }
}

fn run(op: Op) {
    match op.calculate() {
        Some(result) => println!("{op:?} = {result}"),
        None => println!("{op:?} 无法计算:除数为零"),
    }
}

fn main() {
    run(Op::Add(2.0, 3.0));
    run(Op::Div(10.0, 2.0));
    run(Op::Div(1.0, 0.0));
}

输出:

Add(2.0, 3.0) = 5
Div(10.0, 2.0) = 5
Div(1.0, 0.0) 无法计算:除数为零

这个例子完整展示了枚举建模、match 解构、用 Option 表达“计算可能失败”、再用 match 处理结果。这就是 Rust 处理“多种情况 + 可能失败”的标准范式。


六、本篇小结

  1. Rust 的枚举变体可以携带不同类型和数量的数据,是强大的代数数据类型,也能用 impl 定义方法;
  2. Option<T>(Some(T) / None)编码“可能有值”,Rust 没有 null,且 Option<T> 与 T 类型不同,逼迫你在使用前处理空的情况,从根本上避免空指针异常;
  3. match 能匹配变体并取出其数据,必须穷尽所有可能,_ 用于兜底;
  4. match 是表达式,可用于赋值;
  5. 只关心一种情况时用 if let(可带 else);需要“不匹配就提前退出”时用 let-else。

七、动手练习

  1. 定义一个枚举 Shape { Circle(f64), Rectangle(f64, f64), Triangle(f64, f64) },其中 Triangle 携带底和高,实现一个方法计算各自面积(圆:πr²,三角形:底×高/2);
  2. 写函数 fn first_element(items: &[i32]) -> Option<i32> 返回切片第一个元素,空切片返回 None;
  3. 用 match 处理上一题的返回值,有值时打印,无值时打印“空集合”;
  4. 用 if let Some(x) = ... 重写第 3 题中“只关心有值”的分支;
  5. (选做)定义枚举表示链表的两种情况:空 Nil 或一个节点 Cons(i32, Box<List>),体会递归枚举为何需要 Box(第 16 篇详解)。

下篇预告:第 08 篇|包、Crate 与模块系统

随着代码增多,把所有东西都塞进一个文件会变得混乱。下一篇我们学习 Rust 组织大型代码的方式——模块系统:package(包)、crate(编译单元)、module(模块)、路径(path)、use 关键字、pub 可见性,以及如何用工作区(workspace)管理多个相关项目。

理解了模块系统,你才能顺畅地阅读标准库和第三方库的源码,并为自己的大型项目搭好骨架。下篇见。

更多 Rust 学习内容,欢迎访问 云栈社区。




上一篇:Rust 结构体 struct 全解析:定义、方法与所有权
下一篇:RTX Spark 与 MXC:微软英伟达联手把 Windows 变成原生 AI Agent 电脑
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-10-10 05:35 , Processed in 0.101895 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表