枚举值
约 2419 字大约 8 分钟
2026-02-09
这一章你会真正搞懂: 结构体(struct)解决“把数据聚合在一起”; 枚举(enum)解决“一个值只能是几种可能之一”。 然后我们用 Option 把“没有空指针(Null)”这件事讲透,并掌握 Rust 的三套分支武器:match、if let、let...else。
1) 背景引入:struct 管“组合”,enum 管“分支可能性”
结构体(struct)像一个“有字段的盒子”,把多个字段组合成一个整体,比如 Rectangle { width, height }。
但现实世界里,很多值的特点是:
同一时刻只能是几种形态之一,并且每种形态可能携带不同的数据。
比如“形状”可以是 Rectangle / Circle / Triangle; 比如“IP 地址”只能是 IPv4 或 IPv6; 比如“值”要么存在,要么不存在。
这类问题用结构体硬做,会变得别扭:你会被迫造“kind 字段 + data 字段”的组合,还要自己维护“kind 和 data 是否一致”。
Rust 用 **枚举(enum)**来表达这种“可枚举的可能性”。
术语:
枚举(enum):一个类型,但它的值只能是若干 变体(variant) 中的一个
变体(variant):枚举列出来的每一种可能,比如 V4 / V6
2) 核心原理解析:enum = “同一类型下的多种形态”,而且可以给每种形态带数据
2.1 最简单的 enum:只表达“类别”
我们先定义一个“IP 地址类型”:
// 定义一个枚举:列出 IP 地址的两种可能类型
enum IpAddrKind {
V4,
V6,
}创建枚举值时,使用 :: 访问变体(因为变体在枚举的命名空间里):
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;你可以写一个函数接受 任何一种变体:
// route 接受一个 IpAddrKind,不关心它是 V4 还是 V6
fn route(ip_kind: IpAddrKind) {}
route(IpAddrKind::V4);
route(IpAddrKind::V6);这里的关键是:IpAddrKind::V4 和 IpAddrKind::V6 虽然不同,但它们“同属 IpAddrKind 类型”。 这就是 enum 的威力:让不同可能性仍然可被当作“一个类型”处理。
2.2 给 enum 的变体“直接带数据”:不再需要额外 struct
你很快会发现:只有 V4/V6 不够,我们还想存地址本身。
很多人会先写一个 struct 把 kind 和 address 打包:
// 方案 A:enum + struct
enum IpAddrKind {
V4,
V6,
}
struct IpAddr {
kind: IpAddrKind, // 类型信息
address: String, // 数据
}这能用,但有点“绕”:数据其实是属于变体的。 更直接的表达是:让变体本身携带数据。
// 方案 B:把数据放进变体里(更自然)
enum IpAddr {
V4(String),
V6(String),
}
// 创建实例:变体名本身像“构造函数”
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));一个容易忽略但非常重要的细节: 每个变体名都会自动成为一个构造器函数 IpAddr::V4(...) 这件事,本质是“调用一个返回 IpAddr 的构造器”。
2.3 enum 的更强能力:不同变体可以带“不同类型、不同数量”的数据
IPv4 地址天然是 4 段 0~255 的数,而 IPv6 往往用字符串形式表达。 这时 struct 就不舒服了(它只能有一个 address 字段类型),而 enum 可以非常自然:
// V4 带 4 个 u8;V6 带 String
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));2.4 标准库怎么做:enum 里放 struct,分工更清晰
标准库里 IpAddr 的风格是:变体里不是直接塞基础类型,而是塞更专门的结构体:
struct Ipv4Addr { /* ... */ }
struct Ipv6Addr { /* ... */ }
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}理解它的设计意图很重要:
IpAddr 负责“这是 V4 还是 V6”
Ipv4Addr/Ipv6Addr 负责“各自地址的内部结构与校验”
这就是 Rust 常见的组合方式: enum 管分支,struct 管细节。
3) 实操主线:一个 enum 里塞多种结构——Message 示例
下面这个枚举非常典型:不同变体携带不同数据结构。
// Message 表示四种不同消息形态
enum Message {
Quit, // 1) 不带数据
Move { x: i32, y: i32 }, // 2) 嵌套“具名字段”(像结构体)
Write(String), // 3) 带一个 String
ChangeColor(i32, i32, i32), // 4) 带三个 i32
}如果你不用 enum,而是用四个 struct,会发生什么?
// 用四个结构体也能表达同样数据,但它们是四种不同类型
struct QuitMessage;
struct MoveMessage { x: i32, y: i32 }
struct WriteMessage(String);
struct ChangeColorMessage(i32, i32, i32);问题是:你很难写一个函数“一次性处理这四种类型”,因为它们不是同一种类型。 而 enum 是一个类型,所以可以统一处理:
“只要是 Message,就能进这个函数;具体是哪种 Message,再分支处理。”
3.1 enum 也能写方法:impl Message
enum 和 struct 一样,也可以用 impl 绑定行为:
impl Message {
// &self:只借用,不消费、不修改
fn call(&self) {
// 这里通常会 match self 来执行不同逻辑(后面会讲)
}
}
let m = Message::Write(String::from("hello"));
m.call();4) 扩展思考:Rust 为什么不设计 Null,而是设计 Option?
很多语言有空值(Null),它代表“没有值”。 问题在于:Null 会混进任何引用/对象里,导致“以为有值但其实没有”的灾难。
Rust 的设计选择是:
不提供 Null,但用 enum 来编码“有值 / 无值”。
这个 enum 就是 Option<T>(Option<T>):
// 标准库中的定义(概念上就是这样)
enum Option<T> {
None,
Some(T),
}它非常常用,因此被放进 prelude: 你可以直接写 Some(5)、None,不必写 Option::Some(5)。
4.1 为什么 Option 比 Null 更安全?
因为 Option<T> 和 T 是 不同类型。 编译器不允许你把“可能没有值”的东西当成“必然有值”的东西用。
比如这段代码就不能编译:
let x: i8 = 5;
let y: Option<i8> = Some(5);
// 错误:i8 + Option<i8> 没定义
let sum = x + y;这逼着你做一件正确的事:
在使用 y 之前,必须先处理 None 的情况,把 Option<i8> “变成” i8(或做默认值)。
5) match:处理 enum 的“主武器”,并且强制穷尽(exhaustive)
5.1 match 的心智模型:像“硬币分类器”
值掉下来,依次尝试每个分支的模式(pattern),第一个匹配就执行。
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 的结果就是整个表达式的返回值。
5.2 绑定值:从变体里“取出数据”
把 Quarter 扩展为携带州信息:
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
// ...
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState), // Quarter 里带一个州
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
// 模式 Quarter(state) 会把内部值绑定到变量 state
Coin::Quarter(state) => {
println!("State quarter from {state:?}!");
25
}
}
}术语:绑定(bind) Quarter(state) 中的 state 就是把内部数据“拿出来”给代码块用。
5.3 match 必须穷尽:漏一个分支,直接编译不过
以 Option<i32> 为例,加一函数:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None, // 处理无值
Some(i) => Some(i + 1), // 处理有值
}
}如果你漏掉 None:
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
// None 被漏掉 -> 编译器报 non-exhaustive patterns
}
}这就是 Rust 通过类型系统避免“十亿美元错误”的关键机制之一: 编译器强迫你处理“没有值”的情况。
6) 通配与默认分支:other vs _(你要不要这个值?)
如果你只想特殊处理少数值,其余走默认逻辑:
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
// other 会绑定剩余所有值
other => move_player(other),
}如果你不需要这个值,只想“其余情况啥也不干/重掷”,用 _:
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(), // _ 匹配任何值,但不绑定
}甚至可以明确“啥也不做”:
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (), // 单元值:什么都不做
}7) if let:当你只关心“一个模式”时,减少样板代码
你只想处理 Some(max),不想写完整 match:
let config_max = Some(3u8);
if let Some(max) = config_max {
println!("The maximum is configured to be {max}");
}它等价于:
match config_max {
Some(max) => println!("The maximum is configured to be {max}"),
_ => (),
}取舍:
match 更“严谨”:强制穷尽
if let 更“简洁”:适合只处理一个分支
8) let…else:保持“愉快路径(Happy Path)”,失败就提前返回
当你的逻辑是:
成功:取出内部值继续做事
失败:立刻返回
let...else 非常适合。
8.1 先看 if let 的“产生值或提前返回”写法(略别扭)
fn describe_state_quarter(coin: Coin) -> Option<String> {
let state = if let Coin::Quarter(state) = coin {
state
} else {
return None;
};
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
}8.2 用 let…else 改写:结构更清晰
fn describe_state_quarter(coin: Coin) -> Option<String> {
// 如果 coin 不是 Quarter,直接 return None
let Coin::Quarter(state) = coin else {
return None;
};
// 走到这里说明一定拿到了 state(愉快路径)
if state.existed_in(1900) {
Some(format!("{state:?} is pretty old, for America!"))
} else {
Some(format!("{state:?} is relatively new."))
}
}关键规则:else 分支必须“发散”(diverge)——通常是 return、break、panic! 等,让控制流结束。
9) 总结:struct + enum 让你的 API “类型安全 + 意图明确”
struct:把相关字段聚合在一起,并用名字表达含义(比如 Rectangle { width, height })
enum:把“只能是几种可能之一”的场景写进类型系统(比如 IpAddr::V4 / V6)
Option<T>:用 enum 编码“有/无”,避免 Null 带来的隐患
match:处理 enum 的主力,且强制穷尽;还能从变体里绑定出数据
if let:只关心一个分支时更简洁
let...else:失败就返回,保持 Happy Path,减少嵌套
贡献者
flycodeu
版权所有
版权归属:flycodeu
