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) 的杀手级特性。