Ziglings 笔记 69: 真正的泛型 (Generics)

戴上巫师帽 🧙‍♂️

在 Ziglings 的第 69 个练习中,我们终于触及了 Zig 强大的 泛型 (Generics) 系统。

如果你习惯了 C++ 的 <T> 或者 Java 的 <T extends ...>,你会惊讶于 Zig 的简洁:泛型只是编译期的函数参数。没有尖括号,没有复杂的约束语法,只有普通的函数调用。

挑战:通用的序列生成器

我们需要编写一个 makeSequence 函数,它能够:

  1. 接收任意类型 T(如 u8, u32)。
  2. 接收任意大小 size
  3. 返回一个固定长度的数组 [size]T

这听起来似乎有些矛盾:函数在编译后应该是固定的,怎么能返回不同大小的数组呢?

解决方案

答案在于 comptime。编译器会根据我们要的类型和大小,为我们自动生成该函数的多个版本。

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

pub fn main() void {
    // 调用同一个函数,却生成了三种完全不同的数组
    const s1 = makeSequence(u8, 3);  // [3]u8
    const s2 = makeSequence(u32, 5); // [5]u32
    const s3 = makeSequence(i64, 7); // [7]i64

    print("s1={any}, s2={any}, s3={any}\n", .{ s1, s2, s3 });
}

// 核心签名:
// 1. T: type 必须是 comptime 的,因为类型只存在于编译期。
// 2. size: usize 也必须是 comptime 的,因为它是返回类型 [size]T 的一部分。
fn makeSequence(comptime T: type, comptime size: usize) [size]T {
    var sequence: [size]T = undefined;
    var i: usize = 0;

    while (i < size) : (i += 1) {
        // @intCast 将 usize 转换为目标类型 T
        // @as 确保加法结果也是 T 类型
        sequence[i] = @as(T, @intCast(i)) + 1;
    }

    return sequence;
}

核心知识点总结

1. 类型作为一等公民

在 Zig 中,type 是一个有效的数据类型。你可以把 u8 赋值给一个变量,也可以把它作为参数传给函数。只要这个过程发生在编译期即可。

2. 单态化 (Monomorphization)

这是 Zig 实现泛型的机制。 当我们调用 makeSequence(u8, 3) 时,编译器实际上在幕后悄悄写了一个类似 makeSequence_u8_3 的新函数。

  • 优点:运行效率极高,没有虚函数表或装箱拆箱的开销。
  • 代价:编译出来的二进制文件体积会变大(因为每个不同的组合都生成了一份代码)。

3. 返回类型推导

注意函数的返回类型是 [size]T。 在 C 语言中,你无法写出 int[n] function(int n) 这样的代码。但在 Zig 中,因为 n (即 size) 是编译期常量,这完全是合法的。这让我们可以编写出极其灵活且类型安全的 API。


后续预告:我们已经学会了泛型函数。那么,我们能不能创建泛型结构体呢?比如一个可以存储任意类型的链表?下一篇我们将学习 Generic Structs。