Ziglings 笔记 65: 类型内省 (Introspection)
代码里的镜子
在 Ziglings 的第 65 个练习中,我们遇到了自恋的纳西索斯 (Narcissus),并借此窥探了 Zig 最引以为傲的特性之一:编译期内省 (Compile-time Introspection)。
在其他语言中,想要在运行时获取一个对象的字段名或类型信息通常很麻烦(需要 RTTI 或反射),而且慢。Zig 采用了不同的路径:它允许代码在编译阶段“检查自己”。
挑战:自我认知
我们需要完成以下任务:
- 让
narcissus对象的me和myself指针指向它自己(自恋的体现)。 - 调用一个静态函数获取其类型。
- 使用
@typeInfo检查其字段,并忽略掉void类型的字段。
解决方案
const print = @import("std").debug.print;
const Narcissus = struct {
me: *Narcissus = undefined,
myself: *Narcissus = undefined,
echo: void = undefined, // void 类型的字段,不占空间
// 静态函数,返回当前类型
fn fetchTheMostBeautifulType() type {
return @This(); // @This() 在这里等价于 Narcissus
}
};
pub fn main() void {
var narcissus: Narcissus = Narcissus{};
// 1. 初始化自引用
narcissus.me = &narcissus;
narcissus.myself = &narcissus;
// 2. 获取类型
// @TypeOf 会分析输入值的类型。
// 这里三个参数解引用后都是 Narcissus 类型。
const Type1 = @TypeOf(narcissus, narcissus.me.*, narcissus.myself.*);
// 3. 调用静态函数
// fetchTheMostBeautifulType 没有 self 参数,所以不能用 narcissus.fetch... 调用
// 必须使用 类型名.函数名 的方式。
const Type2 = Narcissus.fetchTheMostBeautifulType();
print("A {s} loves all {s}es. ", .{
maximumNarcissism(Type1),
maximumNarcissism(Type2),
});
print("He has room in his heart for:", .{});
// 4. 类型反射:获取字段列表
// @typeInfo 返回一个 Tagged Union,我们要访问 .struct 分支下的 .fields
const fields = @typeInfo(Narcissus).@"struct".fields;
// 检查字段类型是否为 void
// 注意:这里的比较是在编译期完成的!
if (fields[0].type != void) {
print(" {s}", .{fields[0].name});
}
if (fields[1].type != void) {
print(" {s}", .{fields[1].name});
}
// 字段 2 是 'echo',类型是 void,所以不会打印
if (fields[2].type != void) {
print(" {s}", .{fields[2].name});
}
print(".\n", .{});
}
// ... 辅助函数省略 ...
核心知识点总结
1. @This()
这是一个方便的内置函数,返回它所处的类型。如果你的结构体名字很长,或者你在写通过 @import 引入的匿名结构体,这个函数就非常有用了。
2. @typeInfo(T)
这是 Zig 元编程的核心。它把一个类型(T)变成了一个数据结构(Struct/Union)。
这就意味着我们可以用普通的 Zig 代码(if, for, switch)来检查类型!
例如,你可以编写一个泛型函数,自动为任意结构体生成 JSON 序列化代码,只需要遍历 @typeInfo(T).struct.fields 即可。
3. void 类型
练习中的 echo: void 字段是一个典型的“零比特类型”。
它虽然在源代码里占了一个位置,但在编译后的二进制文件中,它不占用任何内存空间。
在 if (fields[i].type != void) 中,我们利用这一点过滤掉了没有实际数据的字段。
后续预告:代码里手动写 if (fields[0]) ... if (fields[1]) 太傻了。既然 fields 是一个数组,我们能不能用 for 循环来遍历它?但这需要 for 循环在编译期运行。下一篇,我们将正式通过 Comptime 的大门。