Skip to content

Day 6: 常量、可变性与编译期计算

欢迎来到第六天!在昨天的学习中,我们接触了 const 和 var。今天,我们将深入探讨Zig中“不可变性优先”的设计思想,并引出Zig最强大的特性之一:编译期计算(comptime)。理解 comptime 是掌握Zig零开销抽象和泛型编程的关键。

我们再来巩固一下两者的核心区别:

  • const (Constant): 声明一个在编译时就已知其值的常量。它的值在整个程序生命周期中都不能被改变。这不仅能防止意外修改,还能给编译器提供更多的优化机会。在Zig中,你应该尽可能地使用 const。

  • var (Variable): 声明一个运行时变量。它的值可以在程序执行过程中被多次修改。只有当你确实需要一个可以变化的状态时,才应该使用 var。

const compile_time_value = 10; // 编译时常量
var run_time_value = 20; // 运行时变量
run_time_value = 30; // OK
// compile_time_value = 40; // 编译错误: cannot assign to constant

const 的值必须是一个常量表达式,这意味着它的值必须在编译期就能被计算出来。这引出了 comptime 的概念。

comptime 是一个关键字,用于强制一个表达式或一个代码块在编译期间执行。

const std = @import("std");
const power_of_two = comptime blk: {
var result: u32 = 1;
var i: u32 = 0;
while (i < 10) : (i += 1) {
result *= 2;
}
break :blk result;
}; // 这个块在编译时执行
pub fn main() !void {
// power_of_two 的值是 1024,它被直接硬编码到可执行文件中
std.debug.print("2^10 = {d}\n", .{power_of_two});
}

在上面的例子中,整个 comptime 块里的代码(包括 while 循环)都在编译程序的时候被Zig的解释器执行了。最终,power_of_two 这个常量的值 1024 被计算出来,并像一个普通的字面量一样嵌入到最终的程序中。这就是“零开销抽象”的体现:运行时的开销为零。

Zig的类型推断能力非常强大,即使是复杂的类型也能轻松应对。

  • 字符串字面量: 它的类型是 *const [N:0]u8,一个指向N字节长度、以null结尾的u8数组的常量指针。

    const hello = "hello"; // 类型被推断为 *const [5:0]u8
  • 数组字面量:

    const numbers = [_]i32{ 1, 2, 3 }; // 类型被推断为 [3]i32

    [_] 是一个特殊的语法,表示让编译器根据初始化值的数量来推断数组的长度。

除了值本身的不可变性,Zig还通过指针类型来控制所指向内存的可变性。

  • *T: 一个指向类型为 T 的可变数据的指针。
  • *const T: 一个指向类型为 T 的不可变数据的指针。通过这个指针,你不能修改它所指向的内存。
var x: i32 = 10;
const ptr_mut = &x; // ptr_mut 的类型是 *i32
ptr_mut.* = 20; // OK,可以通过可变指针修改
const ptr_const = &x; // ptr_const 的类型也是 *i32,但我们可以转换它
const ptr_const_explicit: *const i32 = &x;
// ptr_const_explicit.* = 30; // 编译错误: cannot assign to constant

使用 *const T 可以让你在函数接口中明确表示“我只会读取这块数据,不会修改它”,这是一种强大的API设计工具。

comptime 可以与 if 结合,根据编译期的条件来选择性地编译代码。这常用于处理平台差异。

const std = @import("std");
const builtin = @import("builtin");
const message = comptime if (builtin.os.tag == .linux) {
"Running on Linux!";
} else if (builtin.os.tag == .windows) {
"Running on Windows!";
} else {
"Running on another OS.";
};
pub fn main() !void {
std.debug.print("{s}\n", .{message});
}

当你编译这段代码时,只有一个分支会被编译进最终的程序。如果你在Linux上编译,else if 和 else 分支的代码会像从未存在过一样被完全丢弃。

回到昨天的“温度转换器”练习。

  1. 打开你的 temp_converter.zig 文件。
  2. 审视你声明的每一个变量。思考一下:这个变量的值在声明之后是否真的需要改变?
  3. 尝试将所有能用 const 声明的 var 变量都改为 const。
  4. 思考一下,为什么 celsius 和 fahrenheit 都适合用 const?

这个练习的目的是培养“默认不可变”的思维习惯。

问:我可以在 const 声明中使用一个函数的返回值吗?

答:可以,但前提是这个函数也必须是 comptime 的,并且它的所有输入也必须是编译期常量。

fn comptime add(a: comptime_int, b: comptime_int) comptime_int {
return a + b;
}
const result = add(2, 3); // OK, result 的值是 5

问:comptime 代码块和普通代码块有什么限制?

答:comptime 代码块在编译时由Zig的内部解释器执行,它不能执行真正的IO操作或进行系统调用。它操作的是编译期的值和类型,而不是运行时的内存。

今天,我们深入理解了Zig中 const 的重要性以及其与编译期计算 comptime 的紧密联系。comptime 是Zig的“超级大国”,它使得在不牺牲性能的前提下编写高度抽象和可重用的代码成为可能。从根据平台选择代码,到实现泛型数据结构,comptime 都是核心技术。

明天,我们将学习Zig中的各种运算符,以及它们在不同编译模式下的溢出行为。