枚举与联合体 —— C 的分离与不安全 vs Rust 的统一与安全
C 的 enum(命名整数常量)和 union(类型不安全的重叠存储)是分离的,
"带标签的联合体"需要程序员手动组合两者,极易出错。
Rust 的 enum 是代数数据类型(ADT)——每个变体可以携带不同类型的数据,
并通过 match 提供穷尽性检查,从根本上消除了 C 的标签-数据不匹配问题。
一、C 的 enum:看起来像类型,实际上是整数
1.1 C 的 enum 就是整数常量
// C 中的枚举
enum Color {
RED, // = 0
GREEN, // = 1
BLUE // = 2
};
// 但实际上就是整数:
enum Color c = RED;
int i = c; // ✅ 隐式转换为 int(c = 0)
c = 42; // ✅ 可以赋值任意整数——编译器只给 warning(或不给)
c = 1000; // ✅ 1000 不在 Color 定义中,但完全合法
C 的 enum 没有类型安全。 它可以被赋予任何整数值,编译器最多发一个 warning,在默认编译选项下甚至没有 warning。
1.2 enum 不能携带数据
// C 中无法表达"一个值可以是整数、字符串或错误码":
enum ValueType {
VAL_INT, // 只能表示"这是一个整数",不能携带整数值
VAL_STRING, // 只能表示"这是一个字符串",不能携带字符串指针
VAL_ERROR // 只能表示"这是一个错误",不能携带错误码
};
// 要携带数据,需要和 union 配合——但那是手动的、不安全的
1.3 switch 没有穷尽性检查
enum Color { RED, GREEN, BLUE };
const char* color_name(enum Color c) {
switch (c) {
case RED: return "red";
case GREEN: return "green";
// ❌ 忘了 BLUE!编译器不报错(除非开启 -Wswitch 且启用)
}
// 如果传入了 BLUE——未定义行为(返回什么?)
}
大多数 C 编译器在默认设置下不会检查 switch 是否覆盖了所有枚举值。即使开启了 -Wswitch,也只是一个 warning——可以被忽略。
1.4 enum 的大小不确定
enum Small { A, B, C };
enum Large { X = 0xFFFFFFFF, Y }; // 需要 32 位
// sizeof(enum Small) 是多少?
// 可能是 1、2、4——取决于编译器实现
// C 标准只说了"足够容纳所有枚举值",没指定具体大小
1.5 C 的 enum 总结
| 问题 | 说明 |
|------|------|
| 类型不安全 | 可以赋任意整数值,编译器不阻止 |
| 不能携带数据 | 只是一个整数标签,没有数据附着 |
| 无穷尽性检查 | switch 漏掉变体,编译器默认不报错 |
| 大小不确定 | 实现可以自由选择底层整数类型 |
二、C 的 union:共享内存,类型不安全
2.1 union 的本质
C 的 union 让多个变量共享同一块内存:
union Data {
int i;
float f;
char str[4];
};
// 所有成员共享同一块内存(4 字节)
union Data d;
d.i = 42; // 写入 int
printf("%d", d.i); // ✅ 42
d.f = 3.14; // 写入 float——覆盖了 i 的内存
printf("%d", d.i); // ❌ 未定义行为!读取 float 的内存作为 int
union 不知道当前哪个成员是"活跃的"。 你可以写入一个成员,然后读取另一个——编译器不会阻止,但这是未定义行为(或实现定义的行为)。
2.2 union 的主要问题
// 问题 1:类型不安全——可以读任何成员
union Data d;
d.i = 42;
printf("%f", d.f); // ❌ 编译通过,但行为未定义
// 问题 2:没有判别式——当前"活跃"的成员是什么?
// 没人知道,全靠程序员记住
// 问题 3:复杂数据结构中的 union 可能被误用
struct Message {
enum MsgType type; // 判别式
union {
int int_val;
char* str_val;
double double_val;
} data; // 数据
};
struct Message m;
m.type = MSG_INT;
m.data.int_val = 42;
// 但没有任何东西阻止你这样做:
m.data.str_val = "hello"; // 即使 type 还是 MSG_INT!
print_message(m); // 内部根据 type=MSG_INT 读取 int_val
// 但实际存的是 char*——灾难
2.3 C 中"带标签的联合体"的手动模式
C 程序员为了解决"一个值可以是多种类型之一"的问题,手动组合 enum + union + struct:
enum ShapeType {
SHAPE_CIRCLE,
SHAPE_RECT,
SHAPE_TRIANGLE,
};
struct Shape {
enum ShapeType type; // 标签——告诉 union 中哪个是活跃的
union {
struct { double radius; } circle;
struct { double w, h; } rect;
struct { double a, b, c; } triangle;
} data; // 数据——根据 type 决定读哪个成员
};
这个模式的问题:
// 问题 1:标签和数据可能不一致
struct Shape s;
s.type = SHAPE_CIRCLE;
s.data.rect.w = 10; // ❌ type 是 CIRCLE,但写入了 rect 的数据
s.data.rect.h = 20;
area(&s); // 按 CIRCLE 读取 radius——读到的是 w 的位模式
// 问题 2:增加新变体时,需要手动找到所有 switch
// 如果有 50 个地方 switch(s->type),必须一个一个改
// 漏掉一个就是 bug,编译器不报错
// 问题 3:每个 Shape 都需要额外的 type 字段(内存开销)
// sizeof(struct Shape) = sizeof(enum ShapeType) + sizeof(union {...}) + padding
2.4 C 的 enum + union 总结
| 问题 | 说明 |
|------|------|
| 标签-数据不匹配 | type 说 CIRCLE,但 data 存的是 rect——编译通过,运行出错 |
| 类型不安全 | 可以从错误的 union 成员读取,编译器不阻止 |
| 无穷尽性检查 | 新增变体后,漏掉某个 switch 分支不报错 |
| 内存浪费 | tag 和 data 分开存储,额外的 padding 和对齐开销 |
| 工程负担 | 需要手动维护 tag 和 data 的一致性,每个 switch 都要手动更新 |
三、Rust 的 enum:统一了标签与数据
3.1 枚举变体可以携带数据
Rust 的 enum 直接解决了 C 中 enum + union 分离的问题——每个变体可以携带不同类型的数据:
enum Shape {
Circle { radius: f64 }, // 变体携带 struct 字段
Rect { w: f64, h: f64 },
Triangle { a: f64, b: f64, c: f64 },
}
// 使用时,标签和数据始终在一起:
let c = Shape::Circle { radius: 5.0 };
let r = Shape::Rect { w: 3.0, h: 4.0 };
标签和数据永远不会不一致——因为它们是一起定义的、一起创建的。 不存在 C 中那种"type 是 CIRCLE 但 data 存的是 rect"的情况。
3.2 更丰富的变体形式
Rust 的枚举变体可以有三种形式:
// 1. 无数据变体(类似 C 的 enum)
enum Color {
Red,
Green,
Blue,
}
// 2. 元组变体(匿名数据)
enum Status {
Ok, // 无数据
Err(String), // 单个字符串
Code(i32, String), // 两个匿名值
}
// 3. 结构体变体(具名字段)
enum Message {
Quit, // 无数据
Move { x: i32, y: i32 }, // 命名字段
Write(String), // 元组变体
ChangeColor { r: u8, g: u8, b: u8 }, // 命名字段
}
3.3 类型安全的保证
// Rust 中不能给枚举赋任意整数值:
let c: Color = Color::Red;
// let c: Color = 42; // ❌ 编译错误:42 不是 Color
// 不能从错误的变体读取数据:
let s = Shape::Circle { radius: 5.0 };
// println!("{}", s.w); // ❌ 编译错误:Circle 没有 w 字段
3.4 内存布局:比 C 更高效
Rust 编译器会对枚举的内存布局进行优化:
enum OptionalInt {
Some(i32),
None,
}
// Rust 不会存储为 "tag (4 字节) + data (4 字节) + padding"
// 而是利用"空指针优化"(niche optimization):
// None 用 i32 中不可能的值来表示(如 i32::MAX + 1 不可能的值)
// 所以 OptionalInt 的大小 == i32 的大小 == 4 字节
// 不需要额外的 tag!
use std::mem::size_of;
assert_eq!(size_of::<OptionalInt>(), 4);
// 和 size_of::<i32>() 一样!没有额外开销
// 更常见的例子:
assert_eq!(size_of::<Option<&i32>>(), size_of::<&i32>());
// Option<&T> 和 &T 一样大——None 用 NULL 指针表示
// C 中用 NULL 表示"没有值",但类型系统不知道
// Rust 中 Option<&T> 明确区分 Some/None,且零额外开销
Rust 的 enum 不仅比 C 的 enum+union 更安全,而且往往更紧凑。 没有 tag 字段、没有 padding、没有"两个地方存信息"。
3.5 Option<T> 和 Result<T, E>——最经典的枚举
标准库中最常用的两个枚举,都是 Rust 的 enum:
// 可能不存在
enum Option<T> {
None,
Some(T),
}
// 可能失败
enum Result<T, E> {
Ok(T),
Err(E),
}
// 使用 Option:
let name: Option<String> = find_user(id);
match name {
Some(n) => println!("找到: {}", n),
None => println!("未找到"),
}
// 使用 Result:
let file: Result<File, Error> = File::open("config.txt");
match file {
Ok(f) => process(f),
Err(e) => println!("打开失败: {}", e),
}
在 C 中,"可能不存在"用 NULL 表示,"可能失败"用返回值 + errno 表示——都是运行时的约定,编译器不检查。Rust 用 Option 和 Result 把这些情况编码到类型系统中——必须在编译期处理它们。
四、补充:Rust 的 union——只有必要时才用
Rust 也有 union,但它被标记为 unsafe,且很少使用:
// Rust 的 union
union Data {
i: i32,
f: f32,
}
let d = Data { i: 42 };
// println!("{}", d.f); // ❌ 不安全的!不能直接读非活跃成员
// 需要 unsafe:
unsafe {
println!("{}", d.f); // ⚠️ 程序员保证 d 中当前是 f32
}
在 Rust 中,99% 的情况下你不需要 union——用 enum 就好了。 union 只在以下场景需要:
-
FFI 调用 C 的 union
-
极低级别的性能优化(和 C 交互时复用内存)
五、match:模式匹配的穷尽之美
5.1 基本 match
match 是 Rust 中处理枚举的主要方式:
enum Shape {
Circle { radius: f64 },
Rect { w: f64, h: f64 },
}
fn area(shape: Shape) -> f64 {
match shape {
Shape::Circle { radius } => 3.14159 * radius * radius,
Shape::Rect { w, h } => w * h,
}
// 编译器检查:所有变体都已覆盖?
// 是——编译通过 ✅
// 否——编译错误 ❌(穷尽性检查)
}
5.2 穷尽性检查(Exhaustiveness)
这是 match 最重要的特性:编译器确保所有可能性都被处理。
fn area(shape: Shape) -> f64 {
match shape {
Shape::Circle { radius } => 3.14159 * radius * radius,
// ❌ 编译错误:没有处理 Shape::Rect
}
}
// error: non-exhaustive patterns: `Rect` not covered
对比 C 的 switch:
// C —— 漏掉变体,编译器不报错
double area(enum ShapeType type, union ShapeData data) {
switch (type) {
case SHAPE_CIRCLE:
return 3.14159 * data.circle.radius * data.circle.radius;
// ❌ 忘了 SHAPE_RECT——编译器沉默
// 运行到 RECT 时:返回未初始化的值,或执行错误的计算
}
}
5.3 _ 通配符
当你只关心部分变体时,可以用 _ 处理其余:
fn describe(c: Color) -> &'static str {
match c {
Color::Red => "红色",
_ => "不是红色", // 处理 Green 和 Blue
}
}
但要注意:如果之后 Color 新增一个变体:
// 新增后:enum Color { Red, Green, Blue, Yellow }
fn describe(c: Color) -> &'static str {
match c {
Color::Red => "红色",
_ => "不是红色", // ✅ 仍然能编译——通配符覆盖了新变体
// 但逻辑可能是错的——Yellow 被归入了"不是红色"
}
}
通配符让你失去穷尽性检查的保护。 好的做法是尽量显式列出所有变体,只在确实不需要区分时使用 _。
5.4 模式匹配的多种形式
// 1. 匹配字面量
let x = 42;
match x {
0 => "零",
1 | 2 => "一或二", // 多模式用 | 连接
3..=10 => "三到十", // 范围匹配(..= 包含右端点)
_ => "其他",
}
// 2. 解构结构体
struct Point { x: i32, y: i32 }
let p = Point { x: 10, y: 20 };
match p {
Point { x, y } => println!("({}, {})", x, y),
Point { x: 0, y } => println!("x=0, y={}", y), // 只匹配 x=0
}
// 3. 解构枚举
enum Message {
Move { x: i32, y: i32 },
Write(String),
}
match msg {
Message::Move { x, y } => move_to(x, y),
Message::Write(text) => println!("{}", text),
}
// 4. 嵌套解构
match shape {
Shape::Circle { radius } if radius > 10.0 => "大圆",
Shape::Circle { .. } => "小圆",
Shape::Rect { w, h } => "矩形",
}
5.5 守卫(match guards)
在模式后面加 if 条件:
fn classify(n: i32) -> &'static str {
match n {
x if x < 0 => "负数",
0 => "零",
x if x > 0 && x <= 100 => "小正数",
_ => "大正数",
}
}
守卫让你在模式匹配的基础上增加条件判断。
5.6 @ 绑定
当你需要同时匹配模式结构和绑定值:
enum OptionalInt {
Some(i32),
None,
}
match val {
Some(x @ 0..=10) => println!("小数字: {}", x),
Some(x @ 11..=100) => println!("中等数字: {}", x),
Some(x) => println!("大数字: {}", x),
None => println!("没有值"),
}
5.7 if let——只关心一个变体时的简洁写法
let color = Some(Color::Red);
// match 写法——需要写通配符
match color {
Some(c) => println!("有颜色: {:?}", c),
None => {} // 必须写
}
// if let 写法——更简洁
if let Some(c) = color {
println!("有颜色: {:?}", c);
}
// 没有 else 分支,不处理 None——简洁但牺牲穷尽性
5.8 while let——循环中的模式匹配
let mut stack = vec![1, 2, 3];
while let Some(top) = stack.pop() {
println!("{}", top);
}
// 弹出所有元素,直到 stack.pop() 返回 None
5.9 match 是表达式
Rust 中 match 是表达式(有返回值),不是语句:
let description = match shape {
Shape::Circle { radius } => format!("半径 {} 的圆", radius),
Shape::Rect { w, h } => format!("{}x{} 的矩形", w, h),
};
// match 的结果赋给了 description
这比 C 的 switch(没有返回值,需要手动赋值)更简洁、更安全:
// C —— switch 是语句,需要手动管理
const char* description;
switch (type) {
case SHAPE_CIRCLE:
// 需要分配字符串、手动管理内存
// 忘记 break 会 fall-through
break;
case SHAPE_RECT:
break;
}
5.10 match 的编译期优化
Rust 编译器会对 match 进行优化——不是简单的 if-else 链:
// 对于简单的枚举,match 被编译为跳转表(和 C 的 switch 一样高效)
match c {
Color::Red => 0,
Color::Green => 1,
Color::Blue => 2,
}
// 编译为:jmp [jump_table + c * sizeof(pointer)]
六、C vs Rust 完整对比
6.1 对比表
| 维度 | C(enum + union) | Rust(enum) |
|---|---|---|
| 标签与数据 | 分离(两个地方) | 统一(变体自带数据) |
| 类型安全 | 无——可赋任意整数、读任意成员 | 有——类型严格,不能乱读 |
| 穷尽性检查 | 无——switch 漏掉变体不报错 | 有——match 必须覆盖所有变体 |
| 携带数据 | 不能——需要额外 union | 可以——每个变体有自己的类型 |
| 内存布局 | tag + union + padding(可能冗余) | 可能优化为 None + niche(更紧凑) |
| 标签-数据一致性 | 手动维护(容易出错) | 编译器保证 |
| 新增变体时的安全性 | 需要手动更新所有 switch——漏了不报错 | 编译器指出所有未覆盖的 match |
| 处理"没有值" | NULL(类型不安全) | Option<T>(类型安全) |
| 处理"可能失败" | 返回值 + errno(类型不安全) | Result<T, E>(类型安全) |
| 内存效率 | tag(可能 4 字节)+ union + padding | 空指针优化:Option<&T> = &T 大小 |
6.2 同一个功能:C vs Rust
// C —— enum + union + switch
enum JsonValueType {
JSON_NULL, JSON_BOOL, JSON_INT, JSON_STRING, JSON_ARRAY
};
struct JsonValue {
enum JsonValueType type; // 标签
union { // 数据
int bool_val;
long long int_val;
char* str_val;
struct JsonValue* array_val;
} data;
};
// 使用:
void print_json(struct JsonValue* v) {
switch (v->type) { // 如果漏掉一个变体——编译器沉默
case JSON_NULL: printf("null"); break;
case JSON_BOOL: printf("%s", v->data.bool_val ? "true" : "false"); break;
case JSON_INT: printf("%lld", v->data.int_val); break;
// 忘了 JSON_STRING 和 JSON_ARRAY
}
// 如果传入了 JSON_STRING——什么都不打印(bug)
}
// Rust —— enum + match
enum JsonValue {
Null,
Bool(bool),
Int(i64),
String(String),
Array(Vec<JsonValue>),
// 新增变体时:编译器会告诉你所有 match 需要更新
}
fn print_json(v: &JsonValue) {
match v {
JsonValue::Null => print!("null"),
JsonValue::Bool(b) => print!("{}", b),
JsonValue::Int(i) => print!("{}", i),
JsonValue::String(s) => print!("\"{}\"", s),
JsonValue::Array(arr) => {
print!("[");
for item in arr {
print_json(item);
}
print!("]");
}
}
// ✅ 所有变体都已覆盖——编译通过
// 如果 JSON_NULL 被改名为 JsonValue::Null——编译器报错,提示所有使用点
}
七、与 C 程序员的对话
"C 的 enum + union 我用了二十年,没出过问题"
C 程序员:"你说的这些 C 的 enum 和 union 的问题,我早就知道了,写代码时候注意就好了。带标签的联合体模式我用了二十年,没出过问题。"
Rust:"我信你。但问题不在你,在 C 本身:C 把"标签-数据一致性"的责任完全交给了你,但它不给你任何工具来保证一致性。你靠的是经验和纪律——但经验和纪律是人的维度,不是语言的维度。团队里不是每个人都有你的经验,而且你三个月后回来改代码也不一定记得当时的全部约定。"
// C —— "注意就好"
struct JsonValue* parse_json(const char* input);
int get_int(struct JsonValue* v) {
// "作为 parse_json 的使用者,你应该知道返回的是什么类型"
// 但如果你不知道呢?或者 parse_json 的实现在某个版本中变了?
return v->data.int_val; // ❌ 可能 type 是 JSON_STRING,但读了 int
}
// Rust —— 编译器帮你记住
fn parse_json(input: &str) -> Result<JsonValue, Error>;
fn get_int(v: &JsonValue) -> Option<i64> {
match v {
JsonValue::Int(i) => Some(*i), // ✅ 只有 Int 变体才读整数
_ => None, // 其他变体都返回 None
}
}
"Rust 的 match 不就是 switch 吗?"
C 程序员:"match 看起来就是 switch 换个写法,有什么区别?"
Rust:"区别在三个地方:穷尽性(编译器检查所有变体是否已覆盖)、统一标签和数据(不需要 separate enum + union)、match 是表达式(有返回值)。C 的 switch 漏掉一个 case 不报错,Rust 的 match 漏掉一个变体不编译。"
// C —— 漏掉了,编译器沉默
switch (type) {
case INT: process_int(val); break;
case FLOAT: process_float(val); break;
// 忘了 STRING——编译器不报错
}
// Rust —— 漏掉了,编译器报错
match value {
Value::Int(i) => process_int(i),
Value::Float(f) => process_float(f),
// ❌ 编译错误:Value::String 没有被覆盖
}
// error: non-exhaustive patterns: `String` not covered
"union 不是很好用吗?怎么就成了不安全的了?"
C 程序员:"C 的 union 方便啊,同一块内存可以用不同的方式解读——写硬件驱动、网络协议解析的时候特别有用。"
Rust:"Rust 也有 union——但要加
unsafe。这恰好是正确的区隔:当你确实需要'同一块内存用不同方式解读'时(硬件寄存器、协议头解析),Rust 允许你这么做——但必须显式标记 unsafe。99% 的日常编码中你不需要这种能力,enum就够了。"
// C —— union 用于任何地方,包括不需要的地方
union Data { int i; float f; };
void process(union Data d) {
// 这个函数接受 int 还是 float?——不知道
// 调用方的意图是什么?——不知道
}
// Rust —— 99% 的场景用 enum,union 只用在不安全/FFI 场景
enum Data {
Int(i32),
Float(f32),
}
fn process(data: Data) {
match data {
Data::Int(i) => { /* i 是 i32 */ }
Data::Float(f) => { /* f 是 f32 */ }
}
}
八、小结
8.1 C 的问题
C 的 enum:只是命名整数
→ 类型不安全、不能携带数据、switch 无穷尽性检查
C 的 union:内存重叠的多个类型
→ 类型不安全、不知道哪个成员活跃、读取错误成员是 UB
C 的 enum + union 组合(带标签联合体)
→ 标签和数据分离 → 可能不一致
→ 手动维护 → 新增变体容易遗漏
→ 编译器不帮助检查 → 纯靠人的纪律
8.2 Rust 的解决
Rust 的 enum = 代数数据类型
→ 每个变体可携带不同类型的数据
→ 标签和数据统一 → 不可能不一致
→ 类型安全 → 不能赋任意整数,不能读错误的变体
Rust 的 match = 带有穷尽性检查的模式匹配
→ 编译器检查所有变体已覆盖
→ 新增变体时,编译器指出所有需要修改的 match
→ match 是表达式,有返回值
→ 支持解构、守卫、@绑定、范围匹配
8.3 一句话总结
C 把"一个值可以是多种类型之一"拆成了三个分离的概念——
enum(标签)、union(数据)、switch(检查)——程序员必须手动维护它们之间的一致性,编译器不提供任何帮助。Rust 用一个统一的enum(变体自带数据)+match(穷尽性检查)解决了这个问题——标签和数据永不分离,编译器检查每一个使用点,新增变体时不会再有遗漏。