第12章 元编程与泛型

第12章 元编程与泛型

在很多语言中,元编程(Metaprogramming)和泛型(Generics)是两个极其复杂且充满魔法的话题(想想 C++ 的模板元编程)。

但 Zig 再次选择了不同的道路。Zig 没有专门的泛型语法(如 <T>),也没有宏系统。它只有一个核心概念:comptime

通过 comptime,Zig 允许你在编译时运行普通的 Zig 代码。这使得类型本身成为了一等公民,可以像普通值一样被传递、操作和返回。

本章我们将通过一个实战项目——构建一个泛型栈 (Generic Stack),来彻底掌握这些概念。

1. 理解 comptime

comptime 是 “compile time” 的缩写。它告诉编译器:“这段代码请在编译期间执行,而不是在程序运行时执行。”

编译时参数

当函数参数被标记为 comptime 时,这意味着该参数必须在编译时就是一个已知常量。

fn multiply(comptime n: i32, x: i32) i32 {
    return n * x;
}

pub fn main() void {
    // 3 是字面量,编译时已知,合法
    _ = multiply(3, 10); 
    
    // var y = 3;
    // _ = multiply(y, 10); // 错误!y 是运行时变量
}

编译时逻辑

你可以在 comptime 块中编写任意逻辑。如果这些逻辑可以在编译时求值,编译器就会直接把结果嵌入到二进制文件中。

const std = @import("std");

fn fibonacci(n: u32) u32 {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

pub fn main() void {
    // 编译器会计算出 fibonacci(10) 的结果 (55)
    // 编译后的程序中直接存储的就是 55,没有任何函数调用开销
    const result = comptime fibonacci(10);
    
    std.debug.print("Result: {d}
", .{result});
}

2. Zig 的泛型:类型即数据

在 Zig 中,类型 (type) 也是一种值

既然类型是值,那么我们可以编写一个函数,它接收一个类型作为参数,并返回一个新的类型。这就是 Zig 实现泛型数据结构的方式。

泛型函数

让我们写一个泛型的 max 函数:

// T 是一个类型,必须在编译时已知
fn max(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}

test "generic max" {
    // T = u8
    try std.testing.expect(max(u8, 10, 20) == 20);
    // T = f32
    try std.testing.expect(max(f32, 3.14, 9.99) == 9.99);
}

3. 实战:构建泛型栈 (Stack)

现在我们运用这些知识,从零构建一个可以存储任意类型的栈。

设计目标

我们希望这样使用栈:

// 创建一个存储 u32 的栈
const StackU32 = Stack(u32); 
var stack = StackU32.init(allocator);
try stack.push(42);

第一步:定义泛型结构体

我们将编写一个函数 Stack(T),它返回一个结构体类型。

const std = @import("std");
const Allocator = std.mem.Allocator;

// 这是一个返回类型的函数!
pub fn Stack(comptime T: type) type {
    return struct {
        items: []T,           // 存储 T 类型的切片
        capacity: usize,
        allocator: Allocator,

        // 为了方便在方法内部引用自身类型
        const Self = @This(); 

        pub fn init(allocator: Allocator) Self {
            return .{
                .items = &[_]T{}, // 初始为空切片
                .capacity = 0,
                .allocator = allocator,
            };
        }

        pub fn deinit(self: *Self) void {
            if (self.capacity > 0) {
                self.allocator.free(self.items);
            }
        }
        
        // ... 后续实现 push/pop
    };
}

第二步:实现 push

我们需要处理动态扩容。

        pub fn push(self: *Self, item: T) !void {
            // 如果容量不足,进行扩容
            if (self.items.len >= self.capacity) {
                const new_capacity = if (self.capacity == 0) 4 else self.capacity * 2;
                
                // 重新分配内存:allocator.realloc
                // 注意:对于切片,realloc 需要传入旧切片
                const new_items = try self.allocator.realloc(self.items, new_capacity);
                
                self.items = new_items; // realloc 会处理数据移动
                self.capacity = new_capacity;
            }
            
            // 这里有个小技巧:
            // self.items 是一个切片。realloc 可能会返回一个长度等于 new_capacity 的切片
            // 但我们希望 items.len 代表实际元素个数。
            // 在标准库 ArrayList 实现中,通常分开存储 `items.ptr` 和 `items.len`。
            // 为了简化,我们这里手动管理 len。
            
            // 更正规的实现:
            // 我们的 items 字段应该是一个 slice,但它的长度应该反映实际元素个数。
            // 当我们 realloc 时,我们需要小心。
            
            // 让我们修正一下数据结构,使其更健壮:
        }

修正后的结构体与 push 实现:

pub fn Stack(comptime T: type) type {
    return struct {
        // items.len 是实际元素个数
        // items.ptr 是内存起始地址
        // 我们还需要知道分配的内存总大小(容量)
        allocated_slice: []T, 
        items_len: usize,
        allocator: Allocator,

        const Self = @This();

        pub fn init(allocator: Allocator) Self {
            return .{
                .allocated_slice = &[_]T{},
                .items_len = 0,
                .allocator = allocator,
            };
        }

        pub fn deinit(self: *Self) void {
            self.allocator.free(self.allocated_slice);
        }

        pub fn push(self: *Self, item: T) !void {
            if (self.items_len >= self.allocated_slice.len) {
                const new_cap = if (self.allocated_slice.len == 0) 4 else self.allocated_slice.len * 2;
                // 扩容
                const new_slice = try self.allocator.realloc(self.allocated_slice, new_cap);
                self.allocated_slice = new_slice;
            }
            
            self.allocated_slice[self.items_len] = item;
            self.items_len += 1;
        }
        
        pub fn pop(self: *Self) ?T {
            if (self.items_len == 0) return null;
            self.items_len -= 1;
            return self.allocated_slice[self.items_len];
        }
    };
}

第三步:测试泛型栈

现在让我们测试一下这个泛型栈是否能工作。

test "Generic Stack" {
    const allocator = std.testing.allocator;

    // 1. 测试 u32 栈
    var stack_int = Stack(u32).init(allocator);
    defer stack_int.deinit();

    try stack_int.push(10);
    try stack_int.push(20);
    try std.testing.expectEqual(@as(?u32, 20), stack_int.pop());
    try std.testing.expectEqual(@as(?u32, 10), stack_int.pop());
    try std.testing.expectEqual(@as(?u32, null), stack_int.pop());

    // 2. 测试 bool 栈
    var stack_bool = Stack(bool).init(allocator);
    defer stack_bool.deinit();
    
    try stack_bool.push(true);
    try std.testing.expectEqual(@as(?bool, true), stack_bool.pop());
}

总结

  1. comptime 是 Zig 的核心:它消除了对复杂宏系统和模板语法的需求。
  2. 类型即数据:泛型类型只是一个返回 type 的普通函数。
  3. 编译时执行:利用 comptime,你可以在编译阶段完成计算、类型检查甚至生成代码,从而提升运行时性能。

Zig 的这种设计使得元编程变得像写普通代码一样自然。理解了这一点,你就理解了 Zig 语言设计的精髓。


原文出处: https://pedropark99.github.io/zig-book/