Ziglings 笔记 35: 告别魔法数字 (Enums)

给数字起个名字

在上一篇关于 unreachable 的笔记中,我们写了一个微型虚拟机。虽然它能工作,但代码里充斥着 1, 2, 3 这样的“魔法数字”,而且被迫写了一个 else => unreachable 来安抚编译器。

Ziglings 的第 35 个练习向我们展示了更好的解决方案:枚举 (Enums)

挑战:让代码更安全

我们需要改造之前的虚拟机,不再使用 u8 来代表指令,而是使用自定义的枚举类型。

解决方案

这是使用 enum 改造后的代码:

const std = @import("std");

// 1. 定义枚举
// 我们创建了一个名为 Ops 的新类型,它只有三个可能的值。
const Ops = enum { inc, dec, pow };

pub fn main() void {
    // 2. 使用枚举
    // 数组的类型现在是 [_]Ops,这意味着它只能存放 Ops 的成员。
    // 如果你试图在这里写一个 4,编译器会直接报错,而不是等到运行时崩溃。
    const operations = [_]Ops{
        Ops.inc,
        Ops.inc,
        Ops.inc,
        Ops.pow,
        Ops.dec,
        Ops.dec,
    };

    var current_value: u32 = 0;

    for (operations) |op| {
        switch (op) {
            Ops.inc => {
                current_value += 1;
            },
            Ops.dec => {
                current_value -= 1;
            },
            Ops.pow => {
                current_value *= current_value;
            },
            // 3. 不需要 else!
            // 因为编译器知道 Ops 只有三个值,而我们已经全部处理了。
            // 这种“刚刚好”的感觉体现了类型系统的强大。
        }

        std.debug.print("{} ", .{current_value});
    }

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

核心知识点总结

1. 为什么用枚举?

  • 可读性Ops.inc 显然比 1 更容易理解。
  • 安全性:枚举创建了一个封闭的集合。你不可能不小心传入一个无效的指令。

2. 完美的 Switch

这是 Zig 穷尽性检查(Exhaustiveness Check)的高光时刻。

  • 对于 u8:编译器认为有 256 种可能,你必须处理所有情况(通常用 else)。
  • 对于 Enum:编译器知道确切的成员数量。如果你处理了所有成员,就不需要 else
  • 维护性:如果你将来给 Ops 增加了一个新指令 div,编译器会立即报错指出 switch 缺少处理,强迫你更新逻辑。这防止了代码腐烂。

后续预告:枚举只能是一组名字吗?在 Zig 中,枚举其实非常强大,它们甚至可以拥有自己的方法(Methods)。下一篇我们将给枚举加上“超能力”。