C 里 enum 和 union 经常一起出现。enum 表示当前是哪种类型,union 复用同一块内存保存不同数据:
enum ValueKind {
VALUE_INT,
VALUE_FLOAT,
VALUE_TEXT,
};
struct Value {
enum ValueKind kind;
union {
int i;
float f;
const char *text;
} data;
};
这是 C 里的 tagged union。它很常见,也很有用,但正确性靠纪律:
if (value->kind == VALUE_INT) {
printf("%d\n", value->data.i);
}
如果 kind 是 VALUE_TEXT,却去读 data.i,C 类型系统不会拦住你。Rust 处理这类问题时,优先用 enum 把 tag 和数据放进同一个类型。
C enum 更像有名字的整数
C 的 enum 常用来替代裸数字:
enum State {
STATE_IDLE,
STATE_READING,
STATE_ERROR,
};
这比 0、1、2 清楚。但第一层可以把它理解成“有名字的整数常量集合”。很多 C 编译器会把它按整数处理,代码里也常常和 switch 配套:
switch (state) {
case STATE_IDLE:
break;
case STATE_READING:
break;
case STATE_ERROR:
break;
}
如果状态只需要一个名字,没有额外数据,Rust 也可以写成类似形式:
enum State {
Idle,
Reading,
Error,
}
处理分支用 match:
fn describe(state: State) -> &'static str {
match state {
State::Idle => "idle",
State::Reading => "reading",
State::Error => "error",
}
}
match 会做穷尽检查。新增一个状态后,旧的 match 没处理它就会编译失败。
C union 只管复用内存
C 的 union 表示多个字段占用同一块存储:
union Number {
int i;
float f;
};
它不记录当前有效的是哪个字段。写入 i 后再读 f,语义要看具体场景、平台和规则。union 本身没有 tag。
所以真实代码里经常会把 enum 和 union 包在一个 struct 里:
enum TokenKind {
TOKEN_INT,
TOKEN_IDENT,
};
struct Token {
enum TokenKind kind;
union {
int int_value;
const char *ident;
} data;
};
这就是 tagged union。kind 是 tag,data 是 payload。
问题是:tag 和 payload 的关系仍然靠程序员维护。类型允许你写出不一致的数据:
struct Token t;
t.kind = TOKEN_INT;
t.data.ident = "abc";
这个对象从业务语义看是坏的,但 C 的类型结构表达不出“TOKEN_INT 时只能读 int_value”。
Rust enum 把 tag 和数据绑在一起
Rust 直接把这种结构写成一个 enum:
enum Token {
Int(i32),
Ident(String),
}
这里没有单独的 kind 字段,也没有独立的 union data。每个变体自己带数据:
Token::Int(i32)才有整数值Token::Ident(String)才有标识符文本
构造时:
let a = Token::Int(42);
let b = Token::Ident(String::from("name"));
你构造不出“tag 是 Int,但 payload 是字符串”的值,因为 Rust enum 没有把 tag 和 payload 分开放。
这就是 Rust enum 和 C enum 最大的差别:Rust enum 更接近安全的 tagged union。
match 取出对应数据
C 里要先判断 tag,再按约定读 union 字段:
switch (token->kind) {
case TOKEN_INT:
printf("%d\n", token->data.int_value);
break;
case TOKEN_IDENT:
printf("%s\n", token->data.ident);
break;
}
Rust 里 match 同时完成判断和解包:
fn print_token(token: &Token) {
match token {
Token::Int(value) => println!("{value}"),
Token::Ident(name) => println!("{name}"),
}
}
在 Token::Int 分支里拿到的是整数,在 Token::Ident 分支里拿到的是字符串。分支和数据是一一对应的。
如果以后增加一种 token:
enum Token {
Int(i32),
Ident(String),
End,
}
原来的 match 会提示没有处理 Token::End。这比 C 里忘记更新某个 switch 更早暴露问题。
状态机也适合用携带数据的 enum
C 状态结构经常写成这样:
enum ConnState {
CONN_DISCONNECTED,
CONN_CONNECTING,
CONN_CONNECTED,
CONN_ERROR,
};
struct Connection {
enum ConnState state;
int fd;
int retry_count;
int error_code;
};
所有字段一直都在,但不是每个状态都需要:
DISCONNECTED时fd是否有效?CONNECTING时error_code是否有效?CONNECTED时retry_count还有没有意义?ERROR时fd要不要关闭?
Rust 可以写成:
enum Connection {
Disconnected,
Connecting { retry_count: u8 },
Connected { fd: i32 },
Error { code: i32 },
}
现在 Disconnected 根本没有 fd 字段,Error 分支才有 code。很多非法组合从类型上就表达不出来。
Option 和 Result 也是 enum
C 经常用特殊值表示“没有”:
int find_index(const int *items, size_t len, int target)
{
for (size_t i = 0; i < len; i++) {
if (items[i] == target) {
return (int)i;
}
}
return -1;
}
Rust 用 Option:
fn find_index(items: &[i32], target: i32) -> Option<usize> {
for (index, item) in items.iter().enumerate() {
if *item == target {
return Some(index);
}
}
None
}
Option<T> 本质上就是 enum:
enum Option<T> {
Some(T),
None,
}
错误处理里的 Result 也是 enum:
enum Result<T, E> {
Ok(T),
Err(E),
}
这两种类型都来自同一个思路:不要把“有没有值”“成功还是失败”藏在特殊整数、空指针或输出参数里,而是写进类型。
Rust 也有 union,但不是默认选择
Rust 也提供 union:
#[repr(C)]
union Number {
i: i32,
f: f32,
}
但它更接近 C 的底层布局工具,而不是普通业务建模工具。读取 union 字段需要 unsafe,因为编译器不知道当前有效的是哪个字段:
let n = Number { i: 42 };
unsafe {
println!("{}", n.i);
}
普通 Rust 代码里,遇到 C tagged union 的需求,优先考虑 Rust enum。只有在 FFI、ABI 兼容、硬件寄存器映射、二进制布局边界等场景,才考虑 #[repr(C)] union。
不要过早用 _
match 里可以用 _ 表示其他情况:
match token {
Token::Int(value) => println!("{value}"),
_ => println!("other"),
}
这很方便,但在协议、状态机、命令类型这类场景要谨慎。过早使用 _ 会吞掉未来新增的变体,削弱穷尽检查的价值。
如果每个变体都有明确工程意义,优先把分支写全。新增变体时,让编译器提醒你哪些地方需要更新。
这一层先记住什么
C 和 Rust 的对应关系可以先这样看:
C enum 常量 -> Rust enum 无数据变体
C switch -> Rust match
C union -> Rust union,主要用于底层/FFI
C enum + union tagged data -> Rust 携带数据的 enum
特殊值 -1 / NULL -> Option<T>
返回码 + 输出参数 -> Result<T, E>
Rust enum 不是 C enum 的简单换皮。它更像把 C 里的 tag、union payload 和分支处理规则合在一起,让错误组合更难写出来。