Chapter 60Advanced Result Location Semantics

附录F. 高级结果位置语义

概览

结果位置语义(RLS)是驱动 Zig 的零拷贝聚合、类型推断与高效错误传播的“静默引擎”。在附录E体验内联汇编后,我们重新深入编译器,观察 Zig 如何将值直接导向其最终落点。无论是构建结构体、联合体,还是手动填充调用方提供的缓冲,它都能消除临时值。59

Zig 0.15.2 明确了指针对齐与可选结果指针相关的 RLS 诊断,使在构造期间推理数据“居所”更容易。v0.15.2

学习目标

  • 跟踪结构体字面量和强制转换如何将结果位置转发到每个字段而不产生隐藏副本。
  • 当您想要复用调用方拥有的存储,同时仍提供返回值的API时,应用显式结果指针。
  • 使用RLS组合联合体,使每个变体直接写入其自己的有效载荷,而无需在运行时分配临时缓冲。

结构体转发的实践

当你将结构体字面量赋给变量时,Zig 会将该操作重写为一系列字段写入,使各子表达式可继承最终目的地。第一个“菜谱”将若干传感器读数汇总为Report,展示嵌套字面量(Report中的range)如何传递继承结果位置。math.zig

Zig
// ! 使用结构字面量构建统计报告,该字面量转发到调用者的结果位置。
const std = @import("std");

pub const Report = struct {
    range: struct {
        min: u8,
        max: u8,
    },
    buckets: [4]u32,
};

pub fn buildReport(values: []const u8) Report {
    var histogram = [4]u32{ 0, 0, 0, 0 };

    if (values.len == 0) {
        return .{
            .range = .{ .min = 0, .max = 0 },
            .buckets = histogram,
        };
    }

    var current_min: u8 = std.math.maxInt(u8);
    var current_max: u8 = 0;

    for (values) |value| {
        current_min = @min(current_min, value);
        current_max = @max(current_max, value);
        const bucket_index = value / 64;
        histogram[bucket_index] += 1;
    }

    return .{
        .range = .{ .min = current_min, .max = current_max },
        .buckets = histogram,
    };
}

test "buildReport summarises range and bucket counts" {
    const data = [_]u8{ 3, 19, 64, 129, 200 };
    const report = buildReport(&data);

    try std.testing.expectEqual(@as(u8, 3), report.range.min);
    try std.testing.expectEqual(@as(u8, 200), report.range.max);
    try std.testing.expectEqualSlices(u32, &[_]u32{ 2, 1, 1, 1 }, &report.buckets);
}
运行
Shell
$ zig test chapters-data/code/60__advanced-result-location-semantics/01_histogram_report.zig
输出
Shell
All 1 tests passed.

由于字面量.{ .range = …, .buckets = histogram }按字段写入,你可以安全地用var数据填充histogram——不会产生该 16 字节数组的临时副本。36

用于复用的手动结果指针

有时您需要两全其美:为符合人体工程学的调用方提供返回值辅助函数,以及为复用存储的热循环提供就地变体。通过公开一个接收*NumbersparseInto例程,您可以显式确定结果位置,同时仍提供受益于自动省略的parseNumbers函数。4 请注意slice方法接受*const Numbers;从按值参数返回切片会指向临时值并违反安全规则。mem.zig

Zig
// ! 通过指针参数填充结构体,演示手动结果位置。
const std = @import("std");

pub const ParseError = error{
    TooManyValues,
    InvalidNumber,
};

pub const Numbers = struct {
    len: usize = 0,
    data: [16]u32 = undefined,

    pub fn slice(self: *const Numbers) []const u32 {
        return self.data[0..self.len];
    }
};

pub fn parseInto(result: *Numbers, text: []const u8) ParseError!void {
    result.* = Numbers{};
    result.data = std.mem.zeroes([16]u32);

    var tokenizer = std.mem.tokenizeAny(u8, text, ", ");
    while (tokenizer.next()) |word| {
        if (result.len == result.data.len) return ParseError.TooManyValues;
        const value = std.fmt.parseInt(u32, word, 10) catch return ParseError.InvalidNumber;
        result.data[result.len] = value;
        result.len += 1;
    }
}

pub fn parseNumbers(text: []const u8) ParseError!Numbers {
    var scratch: Numbers = undefined;
    try parseInto(&scratch, text);
    return scratch;
}

test "parseInto fills caller-provided storage" {
    var numbers: Numbers = .{};
    try parseInto(&numbers, "7,11,42");
    try std.testing.expectEqualSlices(u32, &[_]u32{ 7, 11, 42 }, numbers.slice());
}

test "parseNumbers returns the same shape without extra copies" {
    const owned = try parseNumbers("1 2 3");
    try std.testing.expectEqual(@as(usize, 3), owned.len);
    try std.testing.expectEqualSlices(u32, &[_]u32{ 1, 2, 3 }, owned.data[0..owned.len]);
}
运行
Shell
$ zig test chapters-data/code/60__advanced-result-location-semantics/02_numbers_parse_into.zig
输出
Shell
All 2 tests passed.

用新值重置Numbers并清零后备数组可确保结果位置准备好复用,即使前一次解析只填充了缓冲区的部分内容。57

联合体变体与分支特定目的地

联合体暴露了相同的机制:一旦编译器知道您正在构造哪个变体,它就将有效载荷的结果位置连接到适当的字段。下面的查找辅助函数要么将字节流式传输到Resource有效载荷中,要么返回格式错误查询的元数据,而不分配中间缓冲区。这种方案可以扩展到流解析器、FFI桥或必须避免堆通信量的缓存。

Zig
// ! 演示转发嵌套结果位置的联合体构造。
const std = @import("std");

pub const Resource = struct {
    name: []const u8,
    payload: [32]u8,
};

pub const LookupResult = union(enum) {
    hit: Resource,
    miss: void,
    malformed: []const u8,
};

const CatalogEntry = struct {
    name: []const u8,
    data: []const u8,
};

pub fn lookup(name: []const u8, catalog: []const CatalogEntry) LookupResult {
    for (catalog) |entry| {
        if (std.mem.eql(u8, entry.name, name)) {
            var buffer: [32]u8 = undefined;
            const len = @min(buffer.len, entry.data.len);
            std.mem.copyForwards(u8, buffer[0..len], entry.data[0..len]);
            return .{ .hit = .{ .name = entry.name, .payload = buffer } };
        }
    }

    if (name.len == 0) return .{ .malformed = "empty identifier" };
    return .miss;
}

test "lookup returns hit variant with payload" {
    const items = [_]CatalogEntry{
        .{ .name = "alpha", .data = "hello" },
        .{ .name = "beta", .data = "world" },
    };

    const result = lookup("beta", &items);
    switch (result) {
        .hit => |res| {
            try std.testing.expectEqualStrings("beta", res.name);
            try std.testing.expectEqualStrings("world", res.payload[0..5]);
        },
        else => try std.testing.expect(false),
    }
}

test "lookup surfaces malformed input" {
    const items = [_]CatalogEntry{.{ .name = "alpha", .data = "hello" }};
    const result = lookup("", &items);
    switch (result) {
        .malformed => |msg| try std.testing.expectEqualStrings("empty identifier", msg),
        else => try std.testing.expect(false),
    }
}
运行
Shell
$ zig test chapters-data/code/60__advanced-result-location-semantics/03_union_forwarding.zig
输出
Shell
All 2 tests passed.

复制到固定大小缓冲时,请如示例所示裁剪长度,以免意外写出有效载荷范围。若需完整保留,请改用切片字段,并配合超出联合值寿命的生命周期管理。10

随手可用的模式

  • return .{ … };视为按字段写入的语法糖——编译器已经知道目标位置,因此可以依靠字面量来提高清晰度。36
  • 在解析或格式化时提供基于指针的*_into变体——它们将RLS转变为有意识的API控制杆,而不是隐式优化。4
  • 联合体承载大有效载荷时,请内联构造,使变体无需堆分配或临时缓冲。8

注意与警示

  • 从按值方法(如fn slice(self: Numbers))返回切片会捕获临时副本;应优先使用指针接收器来保持结果位置稳定。
  • 许多标准库构建器接受结果指针——在重新实现类似的管道代码之前,请先阅读它们的签名。fmt.zig
  • RLS不会绕过任何验证:如果子表达式失败(例如,解析错误),部分写入的目标仍由您控制,因此请记住在重用之前重置或丢弃它。57

练习

  • 扩展buildReport以参数化桶大小,然后检查嵌套循环如何在没有副本的情况下转发其目标位置。36
  • parseInto添加溢出检测,使其拒绝超过可配置限制的整数,并在错误发生时重置结果缓冲。57
  • 当有效载荷超过32字节时,让lookup流入调用方提供的临时缓冲,镜像前一节中的基于指针的模式。4

替代方案与边界情况

  • 对于comptime构造,结果位置可能完全存在于编译时内存中;使用@TypeOf来确认您的数据是否会逃逸到运行时。15
  • 在对接期望你自行管理缓冲的 C API 时,将 RLS 与extern结构体结合,以匹配其布局并避免中间拷贝。33
  • 在微优化之前分析热路径:有时使用std.ArrayList或流式写入器会更清晰,而RLS仍会为您消除中间临时值。array_list.zig

Help make this chapter better.

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