Ziglings 笔记 69: 真正的泛型 (Generics)
戴上巫师帽 🧙♂️
在 Ziglings 的第 69 个练习中,我们终于触及了 Zig 强大的 泛型 (Generics) 系统。
如果你习惯了 C++ 的 <T> 或者 Java 的 <T extends ...>,你会惊讶于 Zig 的简洁:泛型只是编译期的函数参数。没有尖括号,没有复杂的约束语法,只有普通的函数调用。
挑战:通用的序列生成器
我们需要编写一个 makeSequence 函数,它能够:
- 接收任意类型
T(如u8,u32)。 - 接收任意大小
size。 - 返回一个固定长度的数组
[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。