Ziglings 笔记 74: 此时无声胜有声 (Implicit Comptime)

没必要重复说

在 Ziglings 的第 74 个练习中,我们学习了一个反直觉的道理:知道何时不写 comptime 和知道何时写它一样重要

Zig 编译器非常聪明。在很多“显而易见”需要编译期求值的地方(比如全局变量初始化、类型定义),它是自动开启 Comptime 模式的。在这些地方加关键字不仅多余,还会让代码这就“有味道” (Smelly)。

挑战:寻找唯一的 Comptime

我们需要定义一个函数 makeLlamas,它返回一个数组。 这就涉及到了我们在第 70 题学到的知识:如果返回值的类型(数组长度)依赖于参数,那么这个参数必须是编译期已知的。

题目限制我们只能使用一次 comptime 关键字。

解决方案

虽然 llama_count 是全局常量(隐式 comptime),虽然 llamas 的初始化也是在顶层(隐式 comptime),但函数定义的参数默认不是。

const print = @import("std").debug.print;

// 全局作用域:隐式 comptime
const llama_count = 5;

// 全局作用域:隐式 comptime
// 编译器会在这里执行 makeLlamas 函数
const llamas = makeLlamas(llama_count);

// 修复点:函数参数
// 我们需要返回 [count]u8。因为数组长度必须在编译期确定,
// 所以 count 必须标记为 comptime。
fn makeLlamas(comptime count: usize) [count]u8 {
    var temp: [count]u8 = undefined;
    var i = 0;

    // 注意:这里的 while 不需要 inline
    // 因为 count 是 comptime 的,编译器有能力优化这个简单的循环
    while (i < count) : (i += 1) {
        temp[i] = i;
    }

    return temp;
}

pub fn main() void {
    print("My llama value is {}.\n", .{llamas[2]});
}

核心知识点总结

1. 哪里是隐式的?

记住这个列表,并在写代码时以此为准:

  • 文件级/结构体级作用域const x = ...
  • 类型声明fn foo(a: Type) ReturnType 中的类型部分。
  • 内联循环inline while (cond) 中的 cond

2. 为什么参数必须显式?

函数 makeLlamas 可能被用于运行时上下文,也可能被用于编译期上下文。 默认情况下,Zig 假设参数是运行时的(为了灵活性)。 只有当我们强制要求该参数用于定义类型(如 [count]T)或用于 comptime 逻辑时,我们才显式加上 comptime 关键字。

3. 代码的整洁之道

不要在代码里撒胡椒面一样到处撒 comptime。 如果代码能编译通过且运行符合预期,通常说明编译器已经理解了你的意图。过度显式反而会增加阅读负担。


后续预告:我们已经彻底搞懂了 Zig 的语法核心和元编程。接下来的练习将进入一个新的篇章:标准库探秘。我们将从最常用的 Read/Write (IO) 开始。准备好和控制台进行更复杂的交互了吗?