Ziglings 笔记 55: 联合体 (Unions) - 节省内存的变色龙
薛定谔的盒子
在 Ziglings 的第 55 个练习中,我们遇到了 Union (联合体)。
如果说 Struct (结构体) 是一个把所有东西都装进去的背包,那么 Union 就是一个变形金刚玩具盒:它一次只能变一种形态。你不能同时让它既是飞机又是汽车。
挑战:昆虫模拟器
我们用 Insect 联合体来表示昆虫:
- 如果是蜜蜂,我们需要记录它采了多少花 (
u16)。 - 如果是蚂蚁,我们需要记录它是否还活着 (
bool)。
因为这是同一个 Insect 类型,我们需要一种方法在打印时区分它们,防止读取错误的内存字段。
解决方案
为了解决这个问题,我们使用了一个枚举 AntOrBee 作为“标签”,告诉打印函数如何正确地“打开”这个联合体。
const std = @import("std");
// 1. 定义 Union
// 这块内存要么存 u16,要么存 bool,不能同时存。
const Insect = union {
flowers_visited: u16,
still_alive: bool,
};
// 2. 定义辅助枚举
const AntOrBee = enum {
a,
b,
};
pub fn main() void {
// 初始化 Ant (使用 still_alive 字段)
const ant = Insect{ .still_alive = true };
// 初始化 Bee (使用 flowers_visited 字段)
const bee = Insect{ .flowers_visited = 15 };
std.debug.print("Insect report! ", .{});
// 3. 匹配调用
// 关键点:我们必须手动传递正确的标签!
// 如果这里把 .a 传给了 bee,程序在运行时就会崩溃。
printInsect(ant, AntOrBee.a);
printInsect(bee, AntOrBee.b);
std.debug.print("\n", .{});
}
fn printInsect(insect: Insect, what_it_is: AntOrBee) void {
// 4. 根据标签决定访问哪个字段
switch (what_it_is) {
.a => std.debug.print("Ant alive is: {}. ", .{insect.still_alive}),
.b => std.debug.print("Bee visited {} flowers. ", .{insect.flowers_visited}),
}
}
核心知识点总结
1. 内存复用
Union 的最大优势是节省内存。如果有 100 万个昆虫,使用 Struct 我们需要同时为每个昆虫分配 u16 + bool 的空间。而使用 Union,我们只需要分配 max(u16, bool) 的空间。在嵌入式开发或高性能场景中,这一点至关重要。
2. 活跃字段安全性
Zig 对 Union 的访问非常严格。在运行时(Debug 模式),Zig 会记录当前哪个字段是活跃的。
如果你试图访问非活跃字段(例如,insect 初始化为 .a,你却去读 .b 的字段),Zig 会抛出 access of inactive union field 错误。
3. 裸联合体的局限
本练习展示的是 Bare Union。缺点显而易见:我们需要手动维护一个额外的 AntOrBee 变量来标记类型。这既麻烦又容易出错(比如传错了参数)。
幸运的是,Zig 提供了一种更高级的特性叫 Tagged Union,可以自动把 Enum 和 Union 绑在一起。
后续预告:既然手动维护 Enum 和 Union 这么麻烦,Zig 有没有办法把它们合二为一呢?下一篇我们将学习 Tagged Unions,这是 Zig 中实现代数数据类型 (Algebraic Data Types) 的杀手级特性。