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字节数据
}

编译器自动处理:

  1. tag 大小自动选择 — 3 个变体只需 1 字节(u8),不像 C 无脑用 int
  2. 对齐自动计算 — padding 编译器搞定
  3. 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 的简写形式。

posted @ 2026-03-15 10:17  BitWarden  阅读(2)  评论(0)    收藏  举报