Chapter 60Advanced Result Location Semantics

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

概述

结果位置语义(RLS)是推动 Zig 零拷贝聚合、类型推断和高效错误传播的静默引擎。在附录E中试验内联汇编之后,我们现在深入编译器内部,了解 Zig 如何直接将值导向其最终归属。无论是构建 struct、union 还是手动填充调用者提供的缓冲区,它都会消除临时值。59

Zig 0.15.2 澄清了关于指针对齐和可选结果指针的 RLS 诊断,使推理数据在构建期间的位置变得更加容易。v0.15.2

学习目标

  • 跟踪 struct 字面量和强制转换如何在没有隐藏副本的情况下将结果位置转发到每个字段。
  • 当您想要重用调用者拥有的存储同时仍提供返回值 API 时,应用显式结果指针。
  • 将 union 与 RLS 相结合,以便每个变体直接写入其自己的有效负载,而无需在运行时分配临时缓冲区。

实际中的 Struct 转发

当您将 struct 字面量赋给变量时,Zig 会将该操作重写为一系列字段写入,允许每个子表达式继承最终目标。第一个配方将少量传感器读数汇总为一个 Report,演示嵌套字面量(Report 内的 range)如何传递继承结果位置。math.zig

Zig
//! Builds a statistics report using struct literals that forward into the caller's result location.
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 例程,您可以显式确定结果位置,同时仍提供受益于自动消除的 parseNumbers4 注意切片方法如何接受 *const Numbers;从按值参数返回切片会指向临时值并违反安全规则。mem.zig

Zig
//! Demonstrates manual result locations by filling a struct through a pointer parameter.
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

Union 变体和分支特定目标

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

Zig
//! Demonstrates union construction that forwards nested result locations.
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.

复制到固定大小缓冲区时,如所示限制长度,这样您就不会意外地写入有效负载之外。如果您需要全长保留,请切换到切片字段并将其与比 union 值更长生命的生命周期配对。10

需要掌握的模式

  • return .{ … }; 视为逐字段写入的糖——编译器已经知道目标,因此依靠字面量以保持清晰。36
  • 在解析或格式化时提供基于指针的 *_into 变体——它们将 RLS 转变为有意识的 API 杠杆,而不是隐式优化。4
  • 当 union 承载大型有效负载时,内联构建它们,这样变体就不需要堆分配或临时缓冲区。8

注意事项和警告

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

练习

  • 扩展 buildReport 以参数化桶大小,然后检查嵌套循环如何在没有副本的情况下仍转发它们的目标。36
  • parseInto 添加溢出检测,这样它会拒绝超过可配置限制的整数,并在错误触发时重置结果缓冲区。57
  • lookup 在有效负载超过 32 字节时流传到调用者提供的临时缓冲区,镜像前一部分基于指针的模式。4

替代方案和边界情况

  • 对于 comptime 构建,结果位置可能完全存在于编译时内存中;使用 @TypeOf 来确认您的数据是否曾逃逸到运行时。15
  • 与期望您管理缓冲区的 C API 接口时,结合 RLS 与 extern struct,这样您可以匹配它们的布局,同时仍避免中间副本。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.