Ziglings 笔记 73: 编译期守卫 (Comptime Blocks)

在编译时预知未来

在 Ziglings 的第 73 个练习中,我们遇到了一个看似普通但暗藏玄机的函数。

我们想要获取数组中的骆驼(Llama)。通常,如果索引越界,程序会在运行时崩溃。但 Zig 提供了一种更高级的防御手段:如果索引是常量,为什么不直接在编译时就检查它呢?

挑战:让编译器报错

代码中有一个 getLlama 函数,它试图在编译期断言索引是否有效。 为了让这个断言生效,我们需要确保传入的索引 i 在编译期就是已知的。

解决方案

你需要做两件事来修复代码(或者理解代码的意图):

  1. 确保 main 调用时传递的是有效索引(数组长度为 5,最大索引是 4)。
  2. 确保 getLlama 的参数被标记为 comptime
const print = @import("std").debug.print;

const llama_count = 5;
const llamas = [llama_count]u32{ 5, 10, 15, 20, 25 };

pub fn main() void {
    // 修复 1: 索引不能越界
    // 如果这里写 5,因为下面的机制,编译器会直接报错 "assertion failure"
    const my_llama = getLlama(4);

    print("My llama value is {}.\n", .{my_llama});
}

// 修复 2: 强制参数为 comptime
// 加上这个关键字后,i 必须是编译期常量。
fn getLlama(comptime i: usize) u32 {
    // 核心逻辑: comptime assert
    // 这行代码只在编译期运行。
    // 如果 i >= 5,编译过程直接终止,生成一个编译错误。
    // 这样,生成的二进制文件里根本不需要包含边界检查的代码,既安全又高效。
    comptime assert(i < llama_count);

    return llamas[i];
}

fn assert(ok: bool) void {
    if (!ok) unreachable;
}

核心知识点总结

1. comptime 的传染性

一旦你在函数内部使用了 comptime 逻辑(比如依赖参数的静态断言),那么这个参数本身通常也必须是 comptime 的。这就形成了一条从调用者到被调用者的“常量链条”。

2. 零运行时开销 (Zero Runtime Overhead)

请注意,assert 函数虽然定义了,但在最终的可执行文件中,针对 getLlama 的调用,不会有任何 if 判断指令

  • 如果检查通过,编译器直接生成读取内存的指令。
  • 如果检查失败,编译器直接拒绝生成文件。 这就是 Zig 追求的极致性能与安全性的平衡。

3. 任何表达式都可以是 Comptime

练习的注释展示了 comptime 可以放在任何地方:

  • comptime foo(): 立即执行函数。
  • comptime { ... }: 立即执行代码块。 这让 Zig 拥有了类似脚本语言的灵活性,但这脚本是在编译时跑的。

后续预告:我们已经掌握了 comptime 参数。接下来的练习将更进一步,我们将利用 comptime 参数来实现泛型函数。准备好看看 Zig 如何不用尖括号 <T> 就实现泛型了吗?