Chapter 56Builtins Quick Reference

附录B. 内建函数速查

概览

@builtins是编译器的动词;它们描述 Zig 如何看待类型、指针与程序结构,并且无需导入即可在每个文件使用。在第三部分体验了编译期编程之后,本附录汇总最常见的内建函数、其意图与在重度元编程代码中应牢记的表层契约。15

0.15.2 稳定了多项内省助手(@typeInfo@hasDecl@field),并明确了新整数尺寸的截断语义,因此依赖此处总结的行为在实践中更可行。v0.15.2

学习目标

  • 扫描代码库时识别反射内建、算术助手和控制内建之间的区别。
  • 结合类型检查内建构建与用户提供的类型一起工作的适配器。
  • 验证数值转换在范围和安全模式边缘处的运行时行为。

核心反射内建

反射内建为我们提供关于用户类型的结构化信息,而无需获取原始指针或丢弃安全检查。15 下面的示例展示如何形成任何结构的文档化摘要,包括编译时字段、可选有效负载和嵌套数组。

Zig
// ! 使用 @typeInfo 和 @field 总结结构体元数据。
const std = @import("std");

fn describeStruct(comptime T: type, writer: anytype) !void {
    const info = @typeInfo(T);
    switch (info) {
        .@"struct" => |struct_info| {
            try writer.print("struct {s} has {d} fields", .{ @typeName(T), struct_info.fields.len });
            inline for (struct_info.fields, 0..) |field, index| {
                try writer.print("\n  {d}: {s} : {s}", .{ index, field.name, @typeName(field.type) });
            }
        },
        else => try writer.writeAll("not a struct"),
    }
}

test "describe struct reports field metadata" {
    const Sample = struct {
        id: u32,
        value: ?f64,
    };

    var buffer: [256]u8 = undefined;
    var stream = std.io.fixedBufferStream(&buffer);
    try describeStruct(Sample, stream.writer());
    const summary = stream.getWritten();

    try std.testing.expect(std.mem.containsAtLeast(u8, summary, 1, "id"));
    try std.testing.expect(std.mem.containsAtLeast(u8, summary, 1, "value"));
}

test "describe struct rejects non-struct types" {
    var buffer: [32]u8 = undefined;
    var stream = std.io.fixedBufferStream(&buffer);
    try describeStruct(u8, stream.writer());
    const summary = stream.getWritten();
    try std.testing.expectEqualStrings("not a struct", summary);
}
运行
Shell
$ zig test 01_struct_introspection.zig
输出
Shell
All 2 tests passed.

在内联循环中使用 @typeInfo@field,以便编译器在特化后仍优化掉分支。17

值提取助手

诸如 @field@hasField@fieldParentPtr 之类的内建允许您将运行时数据映射回编译时声明,而不违反 Zig 的严格别名规则。以下片段展示如何在保持常量正确性的同时公开父指针。meta.zig

Zig
// ! 演示使用 `@fieldParentPtr` 安全恢复容器指针。
const std = @import("std");

const Node = struct {
    id: u32,
    payload: Payload,
};

const Payload = struct {
    node_ptr: *const Node,
    value: []const u8,
};

fn makeNode(id: u32, value: []const u8) Node {
    var node = Node{
        .id = id,
        .payload = undefined,
    };
    node.payload = Payload{
        .node_ptr = &node,
        .value = value,
    };
    return node;
}

test "parent pointer recovers owning node" {
    var node = makeNode(7, "ready");
    const parent: *const Node = @fieldParentPtr("payload", &node.payload);
    try std.testing.expectEqual(@as(u32, 7), parent.id);
}

test "field access respects const rules" {
    var node = makeNode(3, "go");
    const parent: *const Node = @fieldParentPtr("payload", &node.payload);
    try std.testing.expectEqualStrings("go", parent.payload.value);
}
运行
Shell
$ zig test 02_parent_ptr_lookup.zig
输出
Shell
All 2 tests passed.

@fieldParentPtr 假设子指针有效且正确对齐;在调试构建中将其与 std.debug.assert 结合,以尽早捕获意外误用。37

数值安全内建

数值转换是未定义行为经常隐藏的地方;Zig 通过 @intCast@intFromFloat@truncate 使截断显式,所有这些都遵循安全模式语义。37 0.15.2 改进了这些内建在溢出时发出的诊断,使它们在调试构建中成为可靠的守护者。

Zig
// ! 通过受保护的测试练习数字转换内置函数。
const std = @import("std");

fn toU8Lossy(value: u16) u8 {
    return @truncate(value);
}

fn toI32(value: f64) i32 {
    return @intFromFloat(value);
}

fn widenU16(value: u8) u16 {
    return @intCast(value);
}

test "truncate discards high bits" {
    try std.testing.expectEqual(@as(u8, 0x34), toU8Lossy(0x1234));
}

test "intFromFloat matches floor for positive range" {
    try std.testing.expectEqual(@as(i32, 42), toI32(42.9));
}
test "intCast widens without loss" {
    try std.testing.expectEqual(@as(u16, 255), widenU16(255));
}
运行
Shell
$ zig test 03_numeric_conversions.zig
输出
Shell
All 3 tests passed.

将损失性转换包装在小辅助函数中,使意图保持可读,并将断言集中在共享数字逻辑周围。10

编译期控制与护栏

@compileError@panic@setEvalBranchQuota@inComptime 让你直接控制编译期执行;它们是保持元编程确定性和透明性的安全阀。下面的简短示例在编译时保护向量宽度,并在分析过程中计算小斐波那契数之前提升评估分支配额。15

Zig
// ! 演示使用 @compileError 和 @setEvalBranchQuota 的编译时守卫。
const std = @import("std");

fn ensureVectorLength(comptime len: usize) type {
    if (len < 2) {
        @compileError("invalid vector length; expected at least 2 lanes");
    }
    return @Vector(len, u8);
}

fn boundedFib(comptime quota: u32, comptime n: u32) u64 {
    @setEvalBranchQuota(quota);
    return comptimeFib(n);
}

fn comptimeFib(comptime n: u32) u64 {
    if (n <= 1) return n;
    return comptimeFib(n - 1) + comptimeFib(n - 2);
}

test "guard accepts valid size" {
    const Vec = ensureVectorLength(4);
    const info = @typeInfo(Vec);
    try std.testing.expectEqual(@as(usize, 4), info.vector.len);
    // 取消注释下一行将触发编译时守卫:
    // const invalid = ensureVectorLength(1);
}

test "branch quota enables deeper recursion" {
    const result = comptime boundedFib(1024, 12);
    try std.testing.expectEqual(@as(u64, 144), result);
}
运行
Shell
$ zig test 04_comptime_guards.zig
输出
Shell
All 2 tests passed.

@compileError 立即停止编译单元;谨慎使用,当运行时验证更经济时,优先返回错误。保留注释掉的调用(如示例中所示)以记录失败模式而不破坏构建。12

交叉核对模式

  • 在使用用户类型的可选特性之前,先用 @hasDecl@hasField 驱动重构;这与第 17 章介绍的防御性风格一致。
  • 结合 @TypeOf@typeInfo@fieldParentPtr 以在验证代码中保持诊断清晰——三元组使不变量失败时轻松打印结构信息。
  • 记住某些内建(如 @This)依赖于词法作用域;重新组织文件可能悄然改变其含义,因此在每次重大重组后重新运行测试。36

注意与警示

  • 与分配器交互的内建(@alignCast@ptrCast)仍遵守 Zig 的别名规则;有疑问时依赖 std.mem 辅助函数。3
  • @setEvalBranchQuota 对当前编译期执行上下文是全局的;保持狭窄的配额以避免掩盖无限递归。15
  • 某些实验性内建出现在夜间构建中,但不在 0.15.2 中——在采用新名称前固定你的工具链版本。

练习

  • 构建一个诊断辅助函数,使用 @typeInfo.union 打印任何联合体的标签名称。17
  • 扩展数值转换示例以发出截断前后位模式间的人类可读差异。fmt.zig
  • 编写一个编译期守护程序,拒绝缺少 name 字段的结构体,然后将其集成到通用格式化器流水线中。36

替代方案与边界情况

  • 当某个内建与现有行为重复时,优先使用更高层的 std 助手——标准库常为你封装各类边缘情况。43
  • Reflection against anonymous structs can produce compiler-generated names; cache them in your own metadata if user-facing logs need stability. 12
  • 与 C 交互时,请注意某些内建(如@ptrCast)可能影响调用约定;在部署前请复核 ABI 部分。33

Help make this chapter better.

Found a typo, rough edge, or missing explanation? Open an issue or propose a small improvement on GitHub.