枚举与 match:一个值只能是几种情况之一

枚举与 match:一个值只能是几种情况之一

用 enum 表达"几种之一"并让变体携带数据,靠 match 取值与穷尽性检查,理解 Option 如何取代 null

上一篇的 struct 有一个隐含前提:一个 User 永远同时拥有 email、username、active 这几个字段,一个不缺。但程序里还有另一类需求------一个值只能是几种情况之一,而且不同情况附带的数据还不一样。IP 地址要么是 IPv4 要么是 IPv6;一枚硬币要么是 1 分、5 分、10 分、25 分。这不是"字段的组合",是"从若干可能里选一个"。

Rust 用枚举(enum)表达这件事,用 match 把里面的数据取出来。这一篇讲这两件事,以及由它们撑起来的、你之后几乎每天都会用到的类型:Option

一个值只能是几种之一

先定义一个 IP 地址种类的枚举:

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

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

fn route(ip_kind: IpAddrKind) {}

route(IpAddrKind::V4);
route(IpAddrKind::V6);

两个细节值得留意。第一,变体名字被收在类型名下,用 :: 访问------你之前见过 String::from,那次是关联函数,这次是变体,语法相同。第二,V4V6同一个类型 IpAddrKind 的两个值,所以 route 能同时接收它们。这正是"枚举"这个词的来源:把一个类型所有可能的取值列出来。

Java 的 enum 做的就是这件事,用起来几乎一样。真正的区别在下一步。

变体可以携带数据

上面的 IpAddrKind 只知道"是哪种",不知道具体地址。想连数据一起带上,最直接的办法是每个变体后面加括号:

rust 复制代码
enum Message {
    Quit,                       // 不带数据
    Move { x: i32, y: i32 },    // 具名字段,形式像 struct
    Write(String),              // 带一个 String
    ChangeColor(i32, i32, i32), // 带三个 i32
}

四个变体,各自携带的数据类型和数量都不同。为什么不用 struct 拼出来?你完全可以定义四个 struct------QuitMessageMoveMessageWriteMessageChangeColorMessage,它们能装下同样的数据。但你手里就只剩四个互不相关的类型了:还得额外加一个标签字段记住"这一次到底是哪一个",并且靠你自己保证标签和数据对得上。enum 把这些收进一个类型里:一个 Message一定且只能是四种之一,附带什么数据在类型定义里就写清了。

enum 也能挂方法,写法和 struct 完全一样------impl Message { fn call(&self) { ... } },上一篇讲的三种 self 接收者照旧适用。

match:把数据取出来

有了 enum,就需要一种方式按变体分支、并把变体里的数据取出来。这就是 match

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

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny   => 1,
        Coin::Nickel  => 5,
        Coin::Dime    => 10,
        Coin::Quarter => 25,
    }
}

三个要点。第一,match 是一个表达式 ,不是语句:每个分支箭头 => 右边求值的结果就是整个 match 的值,所以上面不用写 return------函数体最后一个表达式就是返回值,这条规则你在第一篇里见过。

第二,分支按书写顺序依次比较,第一个匹配上的执行,后面的不再看。

第三,模式里可以绑定变量,顺手把变体里的数据取出来:

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

Some(5) 匹配 Some(i)i 就绑定到里面的 5,于是这一支算出 Some(6)。这一点解释了为什么"变体带数据"和"match"要放在同一篇里:数据存进去和取出来用的是同一套模式语法。Coin 里的 Quarter 如果带一个州名(Quarter(UsState)),分支就写成 Coin::Quarter(state) => ...state 直接可用。

必须穷尽,这是最有价值的部分

plus_one 里的 None => None 删掉,编译直接失败:

text 复制代码
error[E0004]: non-exhaustive patterns: `None` not covered

Rust 不但知道你漏了情况,还告诉你是哪一个,甚至会给出补上它的建议。match 的分支必须覆盖所有可能,这是硬性要求。

这条规则值钱在哪?设想 Coin 后来加了一个 HalfDollar 变体。所有没有兜底分支的 match 会立刻编译失败,编译器挨个把你带到需要更新的地方。对照一下:Java 的传统 switch 对枚举不要求穷尽,漏掉的分支静默落空,加了 default 又把未来新增变体的 bug 藏起来;Python 的 match 语句是运行期匹配,没匹配上就继续往下走,没有任何静态检查。Rust 把"我是不是漏了情况"从人的记性变成了编译器的义务。

补一句公平的话:Java 21 起,sealed interface 加 record pattern 的 switch 也能做穷尽性检查,这是同一思路在新版 Java 里的落地。Rust 的不同在于它从第一个版本就这么设计。

需要兜底时用 _,但注意它必须放最后------分支按顺序比较,_ 写在前面会把后面的分支全部挡死,Rust 也会为此给出警告。

Option:用枚举取代 null

现在讲标准库里最重要的 enum。

Rust 没有 null。The Rust Book 的说法很点题:语言设计常被看成"加了哪些特性",但排除了哪些特性同样重要。null 的问题不是"表示没有值"这个想法本身(这想法很必要),而是它的实现方式------nullable 不是某个类型独有的性质,它渗透在所有引用类型里:任何引用都可能是 null,所以使用任何引用前都该检查,而忘记检查就是运行时崩溃。

Tony Hoare 1965 年在 ALGOL W 里引入 null 引用,后来在演讲里称之为"我那个十亿美元的错误"------原话是当时无法抗拒诱惑,因为实现起来太容易了,结果导致无数错误、漏洞和系统崩溃。

Rust 的替代品就是普通枚举:

rust 复制代码
enum Option<T> {
    None,
    Some(T),
}

T 是泛型参数(第十章细讲),现在只需知道 Some 能装任意类型的一个值,而 Option<i32>Option<char> 是两个不同的类型。Option 和它的两个变体都在 prelude 里,直接写 SomeNone 就行,不用加 Option:: 前缀。

关键差别在这一句:Option<T>T 是两个不同的类型

rust 复制代码
let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;  // 编译错误:不能把 i8 和 Option<i8> 相加

编译器不认识这种加法,因为两边类型不同。这带来一个很实在的结果:只要一个值的类型是 i8,你就知道它一定是个有效的 i8,用之前不必检查 null。只有类型写成 Option<i8> 时,你才需要操心"可能没有值"------而这时编译器会盯着你先把 None 那一种情况处理掉,才能拿到里面的 T

换个说法:想让一个值可能缺失,你得在类型上显式声明;一旦声明,用它之前就必须显式处理缺失。

对照 Java:Integer 字段可以为 null,但类型上完全看不出它会不会是 null,编译器不提醒,只有运行时抛 NPE。Java 也有 Optional,但没有机制强制你用,混用 Optional 和裸 null 的代码库反而更难排查。Python 更彻底------任何变量都可能是 None,只能靠类型标注(Optional[...])和外部工具自觉,解释器不管。Rust 的做法是把这份责任整个挪到类型系统和编译器身上。

顺带说明:Option 只是普通枚举,上面讲的 match 规则原样适用,所以"忘了处理没值的情况"这件事,被 E0004 拦在了编译期。这就是 Book 那句话的意思------编译器阻止你忘记处理 None,从而让"误以为有值其实没有"这类 bug 不可能发生。

if let:只关心一个变体时

如果只在乎 Somematch 就得加一条凑数的分支来满足穷尽性:

rust 复制代码
match config_max {
    Some(max) => println!("The maximum is configured to be {max}"),
    _ => (),   // 纯粹为了凑数
}

if let 就是这种场景的简写:

rust 复制代码
if let Some(max) = config_max {
    println!("The maximum is configured to be {max}");
}

等号左边是模式,右边是表达式;匹配上才执行块,max 绑定到里面的值。可以加 else,它的地位相当于 match 里原来那条 _ 分支。

代价要记住:if let 放弃了穷尽性检查。所以判断标准很简单------其他情况我确实不关心,用它;我要保证一个不漏,用 match。Rust 还有一个 let...else,专门处理"不匹配就提前返回"的写法,这里先不展开。

struct 说的是"这个值由这些字段组成",enum 说的是"这个值只能是其中之一"。match 是把 enum 里的数据取出来、并保证每种情况都被处理的那把刀。而 Option 是这套机制最有价值的产物:它把"可能没有值"变成了一个必须在类型上声明、必须显式处理的状态。

顺着同一思路往下,还有一个你很快就会遇到的 enum:Result<T, E>,用来表示"成功或者失败"。它就是 Option 的加强版------同样靠 enum 和 match,只是失败时还带上了出错的原因。下一篇讲它。


参考来源

相关推荐
小灰灰搞电子1 小时前
Rust suppaftp 库详解:基于 FTP 客户端实战指南
开发语言·后端·rust
编码浪子1 小时前
Rust unsafe 与 FFI 互操作生产级实战:把危险关进笼子的四道闸门
开发语言·后端·rust
步行cgn2 小时前
Spring Boot 将配置绑定到第三方对象详解
spring boot·后端·python
传奇开心果编程10 小时前
【xilem0.4基础语法学与练】第13课:Xilem 0.4 最简短代码体现“一切皆设计图“
学习·rust·前端框架
码事漫谈10 小时前
多人共用一个 key,缓存命中率会不会因此降低?
后端
考虑考虑11 小时前
docker compose环境变量替换
运维·后端·自动化运维
武雄(小星Ai)12 小时前
飞书API上传26MB文件偶发失败:错误码9499与空响应体排坑实录
后端·api·排坑实录
徐小夕12 小时前
JitWord 4.0 万字分享:从协同工具到AI Word操作系统,聊聊3年产品创业史
前端·vue.js·后端
geovindu14 小时前
CSharp: Observer Pattern
开发语言·后端·观察者模式·设计模式·c#·.netcore·行为模式