Skip to content
zig 版本:0.13.0

反射

在计算机学中,反射(reflection),是指计算机程序在运行时(runtime)可以访问、检测和修改它本身状态或行为的一种能力。用比喻来说,反射就是程序在运行的时候能够“观察”并且修改自己的行为。

事实上,由于 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;
    // 此处的 foot 函数如果是正常执行会增加 data 的值
    // 但是由于这里使用的是 @TypeOf 故会在编译期进行运算,所以不会出现运行时的副作用
    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,它包含了此类型的所有信息。

它是一个联合类型,有 Struct, Union, Enum, ErrorSet 等变体来储存结构体、联合、枚举、错误集等类型的类型信息。要判断类型的种类,可以使用 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 才能编译通过,这是因为结构体的 “字段类型” std.builtin.Type.StructField中的一个字段是 comptime_int类型,使得StructField没有运行时大小,从而不能在运行时遍历其数组,必须用 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 获得一个结构体的信息,并使用 @Type 构造一个新的类型。构造的新结构体类型和原结构体的字段名和顺序相同,但结构体的内存布局被改为 extern,且每个字段的对齐被改为1。

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

fn ExternAlignOne(comptime T: type) type {
    // 获得类型信息,并断言为Struct.
    comptime var struct_info = @typeInfo(T).Struct;
    // 将内存布局改为 extern
    struct_info.layout = .@"extern";
    // 复制字段信息(原为只读切片,故需复制)
    comptime var new_fields = struct_info.fields[0..struct_info.fields.len].*;
    // 修改每个字段对齐为1
    inline for (&new_fields) |*f| f.alignment = 1;
    // 替换字段定义
    struct_info.fields = &new_fields;
    // 重新构造类型
    return @Type(.{ .Struct = struct_info });
}

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 };

    // 根据子字段 x 的指针来获取父结构体 Point 的地址
    const res = &p == @as(*Point, @fieldParentPtr("x", &p.x));

    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 {
    // 使用 @call 调用函数,其中 .auto 是自动指定调用约定
    const result = @call(.auto, add, .{ 1, 2 });
    std.debug.print("call function add, the result is {}\n", .{result});
}

构建新的类型

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

构建新类型的能力主要依赖于 @Type

@Type

该函数实际上就是 @typeInfo 的反函数,它将类型信息具体化为一个类型。

函数的原型为:

@Type(comptime info: std.builtin.Type) type

参数的具体类型可以参考 此处

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

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

const T = @Type(.{
    .Struct = .{
        // 结构体的内存布局
        .layout = .auto,
        // 对应的字段
        .fields = &.{
            .{
                // 字段的对其方式
                .alignment = 8,
                // 字段名
                .name = "b",
                // 字段类型
                .type = u32,
                // 是否是编译期已知
                .is_comptime = false,
                // 是否有默认值
                .default_value = null,
            },
        },
        // 对应的方法和属性,不可构建
        .decls = &.{},
        // 是否是元组
        .is_tuple = false,
    },
});

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

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

🅿️ 提示

除了常见的类型外,还有以下特殊类型:

  • 关于枚举,还存在一个 EnumLiteral 类型,可以称之为枚举字面量,详细说明见 枚举

WARNING

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

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