Ziglings 笔记 65: 类型内省 (Introspection)

代码里的镜子

在 Ziglings 的第 65 个练习中,我们遇到了自恋的纳西索斯 (Narcissus),并借此窥探了 Zig 最引以为傲的特性之一:编译期内省 (Compile-time Introspection)

在其他语言中,想要在运行时获取一个对象的字段名或类型信息通常很麻烦(需要 RTTI 或反射),而且慢。Zig 采用了不同的路径:它允许代码在编译阶段“检查自己”。

挑战:自我认知

我们需要完成以下任务:

  1. narcissus 对象的 memyself 指针指向它自己(自恋的体现)。
  2. 调用一个静态函数获取其类型。
  3. 使用 @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 的大门。