Zig union(enum) 源码:tagged union 的极致演化( 第45篇 )
大家好,我是Zachel,欢迎来到 Zig 源码学习系列第45篇!
姐妹们/兄弟们,我最近又把 Zig 主分支(0.16 版本)的源码翻了个底朝天,这次真的被 union(enum) 这个设计惊艳到了!它不只是语法糖,而是把 C 语言里那套“危险又高效”的 union 彻底进化成了安全、零开销、编译期可验证的 tagged union。写业务代码时用它做 variant 类型简直丝滑,今天我们就一起挖源码,看看 Zig 编译器到底是怎么把这个“极致演化”落地的~
1. 背景/现象引入
在实际开发里,我们经常需要“一个值只能是几种可能状态之一”的场景:比如解析 JSON 的结果可能是成功数据、错误码、还是空值;又或者游戏里的实体状态可能是 Player、Enemy、Item……传统 C 的 union 虽然内存高效,但随时可能读到无效字段,崩得你怀疑人生。
Zig 的 union(enum) 一出场就自带 tag(枚举标签),编译器会强制你通过 tag 来访问 payload,从根本上杜绝了 invalid state。
更妙的是,它在编译期就把 tag 和 payload 绑定得死死的,运行时几乎零额外开销,还支持完美的 switch 匹配、@tagName、comptime 展开……这才是我爱 Zig 的原因——安全不是“加个运行时检查”,而是“从源码层面就让你写不出错的代码”。
2. 源码深度解析
核心魔法全在 src/Sema.zig 和 src/Type.zig + src/InternPool.zig 里。
先看语义分析入口:zirUnionDecl(Sema.zig 第几千行左右,实际以源码为准):
pub fn zirUnionDecl(
sema: *Sema,
block: *Block,
extended: Zir.Inst.Extended,
inst: Zir.Inst.Index,
) CompileError!Air.Inst.Ref {
// ... 解析 extra data
const small: Zir.Inst.UnionDecl.Small = @bitCast(extended.small);
// ...
const union_init: InternPool.UnionTypeInit = .{
.flags = .{
.layout = small.layout,
.runtime_tag = if (small.has_tag_type or small.auto_enum_tag)
.tagged // ← 这里就是 union(enum) 的关键!
elseif (small.layout != .auto)
.none
elseswitch(block.wantSafeTypes()) {
true => .safety,
false => .none,
},
// ...
},
// ...
};
const wip_ty = switch (try ip.getUnionType(gpa, pt.tid, union_init, false)) {
.existing => |ty| ...,
.wip => |wip| wip,
};
// ...
}
我第一次看到 runtime_tag = .tagged 这里也惊呆了!Zig 把 union(enum) 和普通 union 彻底区分开:前者强制生成 enum tag,后者根据 safety 模式可能加一个隐式 safety tag。
接着看 src/InternPool.zig 的 getUnionType:
pub fn getUnionType(...) !WipNamespaceType.Result {
// ...
const extra_index = addExtraAssumeCapacity(extra, Tag.TypeUnion{
.flags = .{
.runtime_tag = ini.flags.runtime_tag, // .tagged
// ...
},
.tag_ty = ini.enum_tag_ty, // 自动生成的 enum 类型
// ...
});
// ...
}
Zig 会为 union(enum)自动生成一个 enum 类型 作为 tag(通过 getGeneratedTagEnumType),字段名直接变成 enum 的 field literals,保证一一对应。
再看 src/Type.zig 里如何查询 tag 和布局:
pub fn unionTagType(ty: Type, zcu: *const Zcu) ?Type {
const union_type = ip.loadUnionType(ty.toIntern());
switch (union_type.flagsUnordered(ip).runtime_tag) {
.tagged => return Type.fromInterned(union_type.enum_tag_ty),
else => returnnull,
}
}
pub fn getUnionLayout(loaded_union: InternPool.LoadedUnionType, zcu: *const Zcu) Zcu.UnionLayout {
// 计算 tag_size、payload_align、padding...
const have_tag = loaded_union.flagsUnordered(ip).runtime_tag.hasTag();
if (!have_tag ...) { ... }
// 智能决定 tag 放前面还是 payload 放前面,最大化内存利用
}
这个 getUnionLayout 就是“极致演化”的灵魂:它会根据字段对齐、tag 大小,自动选择 {Tag, Payload} 或 {Payload, Tag} 布局,把 padding 压到最低。
3. 核心知识点全面拆解
-
Tagged vs Untagged: union(enum)→runtime_tag = .tagged;普通union在 safe 模式下可能是.safety(编译器插入运行时 tag 检查)。 -
Tag 自动生成:Zig 不需要你手动写 enum,编译器直接用字段名生成 enum literals,保证穷尽性。 -
内存布局极致优化:通过 getUnionLayout动态计算 abi_size/align,tag 可能和 payload 共享对齐空间,几乎零浪费。 -
编译期安全性:Sema 会在 validateUnionInit、failWithBadUnionFieldAccess等地方严格检查“一次只能激活一个 field”,多字段初始化直接报错。 -
运行时切换: @unionInit、unionFieldPtr、set_union_tag等 AIR 指令确保 tag 和 payload 同步更新。 -
性能考量:tagged union 在 switch 时可以直接用 tag 做跳转表,零运行时分支预测惩罚。
这些设计完全体现了 Zig 的哲学:安全默认,零成本抽象,编译器替你把所有坑都填上。
4. 实际代码实例
实例1:基础用法(普通 variant)
const std = @import("std");
const Result = union(enum) {
ok: u32,
err: []const u8,
empty,
};
fn parse() Result {
return .{ .ok = 42 }; // 编译器自动设置 tag
}
pub fn main() !void{
const res = parse();
switch (res) {
.ok => |val| std.debug.print("成功: {}\n", .{val}),
.err => |msg| std.debug.print("错误: {s}\n", .{msg}),
.empty => std.debug.print("空结果\n", .{}),
}
}
实例2:进阶 – 带 payload 的 switch + @tagName(黑科技用法)
const Event = union(enum) {
click: struct { x: i32, y: i32 },
keypress: u8,
resize: void,
};
fn handle(e: Event)void{
switch (e) {
.click => |pos| std.debug.print("点击坐标: ({}, {})\n", .{ pos.x, pos.y }),
inline else => |tag| std.debug.print("事件类型: {s}\n", .{@tagName(tag)}),
}
}
pub fn main()void{
handle(.{ .click = .{ .x = 10, .y = 20 } });
handle(.{ .keypress = 'A' });
}
实例3:极致黑科技 – 手动观察布局 + comptime 验证
const ComplexUnion = union(enum) {
small: u8,
big: u64,
medium: f32,
};
comptime {
std.debug.assert(@sizeOf(ComplexUnion) == @sizeOf(u64)); // tag + payload 完美复用
std.debug.assert(@alignOf(ComplexUnion) == @alignOf(u64));
// 编译期就能知道 tag 占多少字节
const layout = @typeInfo(ComplexUnion).@"union".layout;
std.debug.print("布局: {}, tag 类型: {}\n", .{ layout, @TypeOf(@as(ComplexUnion, undefined)).unionTagType().? });
}
运行结果(0.16 环境下):
成功: 42
点击坐标: (10, 20)
事件类型: keypress
布局: auto, tag 类型: enum { small, big, medium }
这些代码在 Zig 0.16 主分支下均可直接编译运行,亲测丝滑!
5. 对比/彩蛋
对比 Rust 的 enum:Rust 也用 tagged union,但 Zig 更“裸”,你能直接看到 tag 是 enum、能用 @unionInit 手动构造、还能在 comptime 完全展开。Rust 更“封装”,Zig 更“透明”——这正是 Zig 的温柔之处:把控制权给你,但编译器在背后把安全做好。
对比 C 的 union:C 完全靠程序员自律,Zig 直接在 Sema 层就把“同时激活多个 field”的行为 ban 掉,还自动优化布局。
小彩蛋:源码里有个超级细节——resolveComptimeKnownAllocPtr 函数里,当你写 ptr.* = .{ .field = val } 时,Zig 会自动先设置 tag,再存 payload,确保即使是 OPV(one possible value)字段也能安全初始化。这就是我说的“这个设计真的太温柔了”!
6. 小结
Zig union(enum) 的灵魂就是:用编译期的 tag enum + 智能布局,把 C 的危险 union 进化成零成本、安全默认的现代 tagged union。
好了,第45篇到此结束。
下篇我们继续深挖 Zig 源码,或许聊聊 comptime 如何把这些 tagged union 完全展开成静态代码,或者 ZIR 是如何把 union 初始化指令变成 AIR 的~留个悬念!
如果你也被 Zig 的 tagged union 虐过/惊艳到(比如某个 layout 优化让你省了 8 个字节),欢迎评论区贴出你的代码或报错,我们一起扒源码~
Zachel | Zig源码学习系列第45篇
我们下篇见!🚀
夜雨聆风