Ziglings 笔记 56: 完美的结合 (Tagged Unions)

自带说明书的盒子

在上一篇笔记中,为了区分 Insect 联合体里到底是蚂蚁还是蜜蜂,我们不得不笨拙地传递一个额外的枚举参数。如果传错了,程序就会读取错误的内存。

Ziglings 的第 56 个练习向我们展示了终极解决方案:Tagged Union (带标签的联合体)

挑战:让联合体“自知”

我们需要修改 Insect 的定义,让它自己携带类型信息,并重写 printInsect 函数,利用 Switch 语句自动解包数据。

解决方案

这是利用 Tagged Union 优化后的代码:

const std = @import("std");

// 1. 定义标签枚举
const InsectStat = enum { flowers_visited, still_alive };

// 2. 定义 Tagged Union
// 语法:union(枚举名)
// 这相当于告诉编译器:请把 InsectStat 的值和数据存放在一起。
const Insect = union(InsectStat) {
    flowers_visited: u16,
    still_alive: bool,
};

pub fn main() void {
    // 初始化时,Zig 会自动设置内部的标签
    const ant = Insect{ .still_alive = true };
    const bee = Insect{ .flowers_visited = 16 };

    std.debug.print("Insect report! ", .{});

    // 现在的函数调用非常简单,不需要传额外的枚举了
    printInsect(ant);
    printInsect(bee);

    std.debug.print("\n", .{});
}

fn printInsect(insect: Insect) void {
    // 3. 直接 Switch 联合体
    // Zig 会自动检查内部的标签
    switch (insect) {
        // payload capture: |a|
        // 如果是 .still_alive,把里面的 bool 值捕获给变量 a
        .still_alive => |a| std.debug.print("Ant alive is: {}. ", .{a}),
        
        // payload capture: |f|
        // 如果是 .flowers_visited,把里面的 u16 值捕获给变量 f
        .flowers_visited => |f| std.debug.print("Bee visited {} flowers. ", .{f}),
    }
}

核心知识点总结

1. 什么是 Tagged Union?

它是一种数据结构,可以容纳不同类型的值,但在同一时间只能容纳其中一种。与 C 语言的 union 不同,Zig 的 Tagged Union 强制绑定了一个 enum 标签,用来记录当前存储的是哪种类型。这消除了“类型混淆”的风险。

2. 模式匹配 (Pattern Matching)

这个练习展示了 Zig 强大的模式匹配能力。 switch (insect) 实际上做了两件事:

  1. 控制流:根据标签跳转到正确的 case
  2. 数据流:通过 |val| 语法,安全地将内部数据解包(Unwrap)出来供我们使用。

3. 一通百通

回顾一下我们学过的:

  • Optional (?T):本质上就是 union(enum) { null, value: T }
  • Error Union (!T):本质上就是 union(enum) { error_code, value: T }

理解了 Tagged Union,你就理解了 Zig 类型系统的半壁江山。


后续预告:我们已经彻底掌握了基础数据类型。接下来,我们将再次回到 Switch 语句。我们之前只用了最基本的匹配,但如果我想匹配“某个范围”或者“多个值”呢?下一篇我们将学习 Switch 的进阶用法。