Skip to content
zig 版本:0.16.0

反射

在计算机科学中,反射(Reflection)是指程序在执行过程中可以访问、检测和修改自身结构或行为的一种能力。

反射通常包含两个层面:内省(Introspection)——即程序检查自身类型信息的能力;以及中间表示操纵(Intercession)——即程序修改自身结构的能力。

在传统的动态语言(如 Python、Ruby)中,反射发生在运行时。而 Zig 是一门强类型的静态语言,它的反射完全在编译期实现——我们可以在编译期观察已有类型的信息(内省),并根据这些信息构建全新的类型(有限度的中间表示操纵)。这种编译期反射的优势在于:不会引入任何运行时开销,且所有类型错误都能在编译期被捕获。

观察已有类型

zig 提供了不少函数来获取已有类型的信息,如:@TypeOf@typeName@typeInfo@hasDecl@hasField@field@fieldParentPtr@call

@TypeOf

@TypeOf,该内建函数用于使用获取变量的类型。

原型为:@TypeOf(...) type

它接受任意个表达式作为参数,并返回它们的公共可转换类型(使用 对等类型转换),表达式会完全在编译期执行,并且不会产生任何副作用(可以看作仅仅进行类型计算)。

zig
// 会触发编译器错误,因为 bool 和 float 类型无法进行比较
// 无法执行对等类型转换
_ = @TypeOf(true, 5.2);
// 结果为 comptime_float
_ = @TypeOf(2, 5.2);

无副作用是指:

zig
const std = @import("std");
const expect = std.testing.expect;

test "no runtime side effects" {
    var data: i32 = 0;
    const T = @TypeOf(foo(i32, &data));
    try comptime expect(T == i32);
    try expect(data == 0);
}

fn foo(comptime T: type, ptr: *T) T {
    ptr.* += 1;
    return ptr.*;
}

以上这段测试完全可以运行通过,原因在于,@TypeOf 仅仅执行了类型计算,并没有真正地执行函数体的内容,故函数 foo 的效果并不会真正生效!

@typeName

@typeName,该内建函数用于获取类型的名字。

该函数返回的类型名字完全是一个字符串字面量,并且包含其父容器的名字(通过 . 分隔):

zig
const std = @import("std");

const T = struct {
    const Y = struct {};
};

pub fn main() void {
    std.debug.print("{s}\n", .{@typeName(T)});
    std.debug.print("{s}\n", .{@typeName(T.Y)});
}
sh
$ zig build run
main.T
main.T.Y

@typeInfo

@typeInfo,该内建函数用于获取类型的信息。

该函数返回一个 std.builtin.Type,它包含了此类型的所有信息。

它是一个联合类型,使用小写的联合标签来表示具体类型信息;遇到 Zig 关键字时需要使用转义字段名,例如 @"struct"@"union"@"enum",整数类型则是 .int。要判断类型的种类,可以使用 switch 或直接访问相应标签来断言之。

对结构、联合、枚举和错误集合,它保证信息中字段的顺序与源码中出现的顺序相同。

对结构、联合、枚举和透明类型,它保证信息中声明的顺序与源码中出现的顺序相同。

如以下示例中,首先使用 @typeInfo 来获取类型 T 的信息,然后将其断言为一个 @"struct" 类型,最后用 inline for 输出其字段值。

zig
const std = @import("std");

const T = struct {
    a: u8,
    b: u8,
};

pub fn main() void {
    // 通过 @typeInfo 获取类型信息
    const type_info = @typeInfo(T);
    // 断言它为 struct
    const struct_info = type_info.@"struct";

    // inline for 打印该结构体内部字段的信息
    inline for (struct_info.fields) |field| {
        std.debug.print("field name is {s}, field type is {}\n", .{
            field.name,
            field.type,
        });
    }
}

需要注意的是,示例必须使用 inline for 才能编译通过,这是因为我们读取了每个字段的 type。在 Zig 0.16 中,std.builtin.Type.StructField 本身可以作为运行时大小的字段信息读取;只有像字段类型这样的 comptime-only 信息,才需要在编译期用 inline for 处理。

WARNING

获得的类型信息不能用于修改已有类型,但我们可以用这些信息在编译期构建新的类型!

在以下示例中,使用 @typeInfo 获得一个整数类型的长度,并返回和它的长度相同的u8数组类型。当位数不为 8 的整倍数时,产生一个编译错误。

zig
const std = @import("std");

fn IntToArray(comptime T: type) type {
    // 获得类型信息,并断言为Int
    const int_info = @typeInfo(T).int;
    // 获得Int位数
    const bits = int_info.bits;
    // 检查位数是否被8整除
    if (bits % 8 != 0) @compileError("bit count not a multiple of 8");
    // 生成新类型
    return [bits / 8]u8;
}

test {
    try std.testing.expectEqual([1]u8, IntToArray(u8));
    try std.testing.expectEqual([2]u8, IntToArray(u16));
    try std.testing.expectEqual([3]u8, IntToArray(u24));
    try std.testing.expectEqual([4]u8, IntToArray(u32));
}

在以下示例中,使用 @typeInfo 获得一个结构体的信息,并使用 @Struct 构造一个新的类型。构造的新结构体类型和原结构体的字段名和顺序相同,但结构体的内存布局被改为 extern,且每个字段的对齐被改为 1。

zig
const std = @import("std");

fn ExternAlignOne(comptime T: type) type {
    // 获得类型信息,并断言为Struct.
    const struct_info = @typeInfo(T).@"struct";
    // 准备字段名称
    comptime var field_names: [struct_info.fields.len][]const u8 = undefined;
    comptime var field_types: [struct_info.fields.len]type = undefined;
    comptime var field_attrs: [struct_info.fields.len]std.builtin.Type.StructField.Attributes = undefined;

    inline for (struct_info.fields, 0..) |field, i| {
        field_names[i] = field.name;
        field_types[i] = field.type;
        // 设置对齐为 1,其他属性使用默认值
        field_attrs[i] = .{
            .@"align" = 1,
        };
    }

    // 使用 @Struct 构造新类型(extern 布局,对齐为 1)
    return @Struct(.@"extern", null, &field_names, &field_types, &field_attrs);
}

const MyStruct = struct {
    a: u32,
    b: u32,
};

test {
    const NewType = ExternAlignOne(MyStruct);
    try std.testing.expectEqual(4, @alignOf(MyStruct));
    try std.testing.expectEqual(1, @alignOf(NewType));
}

在以上示例中,我们将原类型的类型信息稍作修改,构造了一个新的类型。可以看到,虽然我们修改了得到的 MyStruct 的类型信息,但 MyStruct 本身并没有变化。

@hasDecl

@hasDecl 用于返回一个容器中是否包含指定名字的声明。

完全是编译期计算的,故值也是编译期已知的。

zig
const std = @import("std");

const Foo = struct {
    nope: i32,

    pub var blah = "xxx";
    const hi = 1;
};

pub fn main() void {
    // true
    std.debug.print("blah:{}\n", .{@hasDecl(Foo, "blah")});
    // true
    // hi 此声明可以被检测到是因为类型和代码处于同一个文件中,这导致他们之间可以互相访问
    // 换另一个文件就不行了
    std.debug.print("hi:{}\n", .{@hasDecl(Foo, "hi")});
    // false 不检查字段
    std.debug.print("nope:{}\n", .{@hasDecl(Foo, "nope")});
    // false 没有对应的声明
    std.debug.print("nope1234:{}\n", .{@hasDecl(Foo, "nope1234")});
}

@hasField

@hasField@hasDecl 类似,但作用于字段,它会返回一个结构体类型(联合类型、枚举类型)是否包含指定名字的字段。

完全是编译期计算的,故值也是编译期已知的。

zig
const std = @import("std");

const Foo = struct {
    nope: i32,

    pub var blah = "xxx";
    const hi = 1;
};

pub fn main() void {
    // false
    std.debug.print("blah:{}\n", .{@hasField(Foo, "blah")});
    // false
    std.debug.print("hi:{}\n", .{@hasField(Foo, "hi")});
    // true
    std.debug.print("nope:{}\n", .{@hasField(Foo, "nope")});
    // false
    std.debug.print("nope1234:{}\n", .{@hasField(Foo, "nope1234")});
}

@field

@field 用于获取变量(容器类型)的字段或者容器类型的声明。

zig
const std = @import("std");

const Point = struct {
    x: u32,
    y: u32,

    pub var z: u32 = 1;
};

pub fn main() void {
    var p = Point{ .x = 0, .y = 0 };

    @field(p, "x") = 4;
    @field(p, "y") = @field(p, "x") + 1;
    // x is 4, y is 5
    std.debug.print("x is {}, y is {}\n", .{ p.x, p.y });

    // Point's z is 1
    std.debug.print("Point's z is {}\n", .{@field(Point, "z")});
}

🅿️ 提示

注意:@field 作用于变量时只能访问字段,而作用于类型时只能访问声明。

@fieldParentPtr

@fieldParentPtr 根据给定的指向结构体字段的指针和名字,可以获取结构体的基指针。

zig
const std = @import("std");

const Point = struct {
    x: u32,
};

pub fn main() void {
    var p = Point{ .x = 0 };

    const res = &p == @as(*Point, @fieldParentPtr("x", &p.x));

    // test is true
    std.debug.print("test is {}\n", .{res});
}

@call

@call 调用一个函数,和普通的函数调用方式相同。

它接收一个调用修饰符、一个函数、一个元组作为参数。

zig
const std = @import("std");

fn add(a: i32, b: i32) i32 {
    return a + b;
}

pub fn main() void {
    std.debug.print("call function add, the result is {}\n", .{@call(.auto, add, .{ 1, 2 })});
}

构建新的类型

zig 除了获取类型信息外,还提供了在编译期构建全新类型的能力,允许我们通过非常规的方式来声明一个类型。

Zig 0.16 移除了旧的 @Type。现在应根据要构造的类型选择具体的内建函数,例如 @Struct@Union@Enum@Int@Tuple@Pointer@Fn@EnumLiteral

类型构造内建函数

这些函数把对应类型的描述直接具体化为一个类型。参数形式请参考各内建函数的文档。

以下示例为我们构建一个新的结构体:

zig
const std = @import("std");

// Zig 0.16 使用 @Struct 替代 @Type
const T = @Struct(
    .auto, // layout
    null, // BackingInt
    &.{"b"}, // field_names
    &.{u32}, // field_types
    &.{.{ .@"align" = 8 }}, // field_attrs
);

pub fn main() void {
    const D = T{
        .b = 666,
    };

    std.debug.print("{}\n", .{D.b});
}

🅿️ 提示

除了常见的类型构造函数外,枚举字面量使用 @EnumLiteral 构造,详细说明见 枚举

WARNING

需要注意的是,当前 zig 并不支持构建的类型包含声明(declaration),即定义的变量(常量)或方法,具体原因见此 issue

不得不说,不支持声明极大地降低了 zig 编译期的特性。