Rust学习 03-枚举内存布局
枚举内存布局 — tag + union 模型
核心概念
Rust 的 enum 本质上是编译器自动生成的 tagged union 结构体,类似 C 里手写的 struct + union,但更智能。
Rust vs C 的 tagged union
C 的做法
struct RoughTime {
uint8_t tag; // 偏移 0,1字节
// uint8_t _pad[3]; // 偏移 1-3,编译器插入 padding
union { // 偏移 4,8字节
struct { uint8_t units; uint32_t value; } past;
struct { uint8_t units; uint32_t value; } future;
} data;
};
// sizeof = 12
- tag 通常用
int(4字节),浪费空间 - 对齐、padding 要自己注意
- switch 漏写 case 运行时才出错
Rust 的做法
enum RoughTime {
InThePast(Units, u32), // 变体0:8字节数据
JustNow, // 变体1:无数据
InTheFuture(Units, u32), // 变体2:8字节数据
}
编译器自动处理:
- tag 大小自动选择 — 3 个变体只需 1 字节(u8),不像 C 无脑用 int
- 对齐自动计算 — padding 编译器搞定
- match 穷尽检查 — 漏写变体编译不过
内存布局
┌───────┬─────────┬──────────────────┐
│ tag │ padding │ data │
│ 1字节 │ 3字节 │ 8字节(最大变体)│
└───────┴─────────┴──────────────────┘
总大小 = 1 + 3(padding) + 8 = 12 字节
padding 存在是因为 data 区里有 u32,u32 要求 4 字节对齐,tag 只占 1 字节,所以补 3 字节。
无数据枚举
enum Ordering {
Less, // 0
Equal, // 1
Greater, // 2
}
// size_of::<Ordering>() = 1(只要 1 字节,不是 C 的 4 字节)
没有携带数据的枚举就是一个整数,跟 C 的 enum 一样,但 Rust 按需选最小的整数类型。
Niche Optimization(利基优化)
核心思路:如果数据本身有不可能出现的值,编译器就拿这个值当 None 用,省掉 tag。
典型例子:Option<&T>
&T是引用(类似 C 的指针),在内存里是 8 字节地址- Rust 的
&T必须绑定有效地址,不可能是 0x0 - 0x0 这个值永远不会自然出现 → 编译器拿它表示 None
Option<&T> 的内存:
地址值 = 0x0 → None
地址值 = 0x7FF6A1B2C3D4 → Some(&T)
结果:Option<&T> 只要 8 字节,跟裸指针一样大,不需要额外的 tag。
对比 Option<u32>
u32 所有值(包括 0)都可能被用到,没有"空位",所以必须加 tag:
┌─────┬─────┐
│ tag │ u32 │ = 8 字节(含 padding)
└─────┴─────┘
模式匹配(Pattern Matching)
match = 加强版 switch
match number {
1 => println!("one"),
2 => println!("two"),
_ => println!("other"), // _ 就是 C 的 default
}
C 的 switch 只能匹配整数,Rust 的 match 能匹配整数、字符串、元组、结构体、枚举——什么都能拆。
let 左边是模式(拆包语法)
let 左边不只能放变量名,还能放"拆包规则":
// 最简单:一个变量
let x = 5;
// 拆元组
let (a, b) = (10, 20); // a=10, b=20
// 拆结构体
let Point { x, y } = p; // 等价于 let x = p.x; let y = p.y;
结构体模式
完整写法:Point { x: x, y: y },冒号左边是字段名,右边是处理方式。
冒号右边放两种东西:
- 变量名 → 取出来用:
Point { x: a, y: b }把 x 取出来叫 a - 具体值 → 匹配条件:
Point { x: 0, y }检查 x 是否等于 0
当变量名和字段名相同时可以简写:Point { x: x } → Point { x }
match p {
Point { x, y: 0 } => println!("在 x 轴上, x={}", x), // y 必须等于 0
Point { x: 0, y } => println!("在 y 轴上, y={}", y), // x 必须等于 0
Point { x, y } => println!("其他: ({}, {})", x, y), // 兜底
}
.. 省略不需要的字段
大结构体只取部分字段时,用 .. 表示"剩下的不要了":
let IoctlParams { code, input_len, .. } = params;
// 等价于:
// let code = params.code;
// let input_len = params.input_len;
// output_len, method, access 不管了
元组模式 vs 结构体模式
- 元组
():按位置取值,没有字段名 - 结构体
{}:按名字取值,有字段名
模式跟着数据类型走,数据是什么类型就用对应的模式去拆。
// 元组模式:同时判断两个条件
match (x > 0, y > 0) {
(true, true) => println!("第一象限"),
(false, true) => println!("第二象限"),
(false, false) => println!("第三象限"),
(true, false) => println!("第四象限"),
}
切片模式
匹配数组/切片的长度和内容:
fn parse_args(args: &[&str]) {
match args {
[] => println!("没有参数"),
[cmd] => println!("命令: {}", cmd),
[cmd, file] => println!("{} -> {}", cmd, file),
[cmd, .., last] => println!("{} ... {}", cmd, last), // 取首尾,中间不管
}
}
一句话总结
Rust 的 enum = 编译器量身定做的 tagged union,自动选最优 tag 大小、处理对齐、做 niche 优化,比 C 手写更省空间更安全。模式匹配是 Rust 的核心语法,match/let/for 等位置都能用模式来拆包数据。
@ 绑定(Binding with @)
@ 解决一个矛盾:模式匹配时,想同时检查范围又想拿到值。
Some(n)→ 能拿到值,但没法限制范围Some(1..=10)→ 能限制范围,但拿不到值Some(n @ 1..=10)→ 两者兼得:检查 1~10 范围,值绑定到 n
let msg = Some(5);
match msg {
Some(n @ 1..=10) => println!("got {}", n), // n = 5
_ => {}
}
n @ 1..=10 读作:"匹配 1..=10 这个模式,匹配成功就把值绑定到 n"。
等价于用 if guard 拆成两步:
Some(n) if n >= 1 && n <= 10 => println!("got {}", n)
@ 就是把"取值"和"判断"压缩成一步的语法糖。简单场景不需要用,嵌套场景(如 Some(n @ ...)) 才真正有用。
Option 与 Some/None
Option 是标准库定义的枚举,表达"有值或没值":
enum Option<T> {
Some(T), // 有值,包着一个 T
None, // 没有值
}
<T>是泛型占位符,用的时候决定具体类型:Option<i32>、Option<String>Some/None被自动导入(prelude),全局可用,不需要Option::前缀- 不能直接把
Option当里面的值用,必须通过 match 或 if let 拆开
C 类比:Rust 用 Option 替代了 NULL 指针/返回 -1 等惯例——编译器强制你处理"值可能不存在"的情况。
// C 里忘了判断 NULL → 运行时崩溃
int* p = get_value();
printf("%d", *p); // p 为 NULL 时段错误
// Rust 里不拆 Option → 编译不过,根本不会到运行时
Some(n) 解构
Some(n) 在模式里 = 确认是 Some,把里面的值拆出来叫 n:
let msg = Some(5);
match msg {
Some(n) => println!("{}", n), // n = 5,从 Some 里拆出来了
None => println!("没值"),
}
n 不是固定名字,叫什么都行(x、value、任意变量名)。
Refutability(可反驳性)
模式分两种:
- Irrefutable(不可反驳) — 一定能匹配成功:
let x = 5;、let (a, b) = (1, 2); - Refutable(可反驳) — 可能匹配失败:
Some(n)可能遇到None
规则:有兜底机制的地方可以用 refutable,没有兜底就只能 irrefutable。
| 场景 | 接受哪种模式 | 原因 |
|---|---|---|
let |
irrefutable | 失败了没法处理 |
match 每个 arm |
refutable | 失败了跳下一个 arm |
if let |
refutable | 失败了跳过整个 if |
while let |
refutable | 失败了退出循环 |
| 函数参数 | irrefutable | 失败了没法处理 |
模式使用场景
模式不只在 match 里能用,很多地方都是模式:
if let — 只关心一种情况
当你只关心 Some 不关心 None 时,if let 省掉 match 里空的 _ => {}:
// if let(简洁)
if let Some(n) = msg {
println!("{}", n);
}
// 等价的 match(啰嗦)
match msg {
Some(n) => println!("{}", n),
_ => {}
}
while let — 循环匹配,失败就停
let mut stack = vec![1, 2, 3];
while let Some(n) = stack.pop() {
println!("{}", n); // 输出 3, 2, 1
}
// pop() 返回 None 时循环结束
函数参数 — 直接在参数位置解构
fn print_point((x, y): (i32, i32)) {
println!("x={}, y={}", x, y);
}
print_point((3, 7)); // 直接把元组拆成 x 和 y
match 穷尽性
match 的每个 arm 可以是 refutable,但所有 arm 加起来必须覆盖全部情况,否则编译报错。不想处理的用 _ => {} 兜底。
一句话总结
@ = 匹配+绑定二合一;Option = 强制处理"有值/没值"的枚举;Refutability = 决定模式能用在哪里;if let/while let = match 的简写形式。

浙公网安备 33010602011771号