概述
结果位置语义(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
//! 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);
}
$ zig test chapters-data/code/60__advanced-result-location-semantics/01_histogram_report.zigAll 1 tests passed.因为字面量 .{ .range = …, .buckets = histogram } 逐字段写入,您可以安全地使用 var 数据播种 histogram——永远不会产生 16 字节数组的临时副本。36
用于重用的手动结果指针
有时您想要两个世界:对符合人体工程学的调用者提供返回值辅助函数,以及对重用存储的热循环提供就地变体。通过公开一个接收 *Numbers 的 parseInto 例程,您可以显式确定结果位置,同时仍提供受益于自动消除的 parseNumbers。4 注意切片方法如何接受 *const Numbers;从按值参数返回切片会指向临时值并违反安全规则。mem.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]);
}
$ zig test chapters-data/code/60__advanced-result-location-semantics/02_numbers_parse_into.zigAll 2 tests passed.用新值重置 Numbers 并清零支持数组可确保结果位置准备好重用,即使之前的解析仅填充了缓冲区的一部分。57
Union 变体和分支特定目标
Union 暴露了相同的机制:一旦编译器知道您正在构建哪个变体,它就将有效负载的结果位置连接到适当的字段。下面的查找辅助函数将字节流传入 Resource 有效负载或返回格式错误查询的元数据,而不分配中间缓冲区。同样的方法可以扩展到流解析器、FFI 桥或必须避免堆内存传输的缓存。
//! 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),
}
}
$ zig test chapters-data/code/60__advanced-result-location-semantics/03_union_forwarding.zigAll 2 tests passed.复制到固定大小缓冲区时,如所示限制长度,这样您就不会意外地写入有效负载之外。如果您需要全长保留,请切换到切片字段并将其与比 union 值更长生命的生命周期配对。10
需要掌握的模式
注意事项和警告
练习
替代方案和边界情况
- 对于
comptime构建,结果位置可能完全存在于编译时内存中;使用@TypeOf来确认您的数据是否曾逃逸到运行时。15 - 与期望您管理缓冲区的 C API 接口时,结合 RLS 与
externstruct,这样您可以匹配它们的布局,同时仍避免中间副本。33 - 在微优化之前分析热路径:有时使用
std.ArrayList或流式写入器更清晰,RLS 仍会为您擦除中间临时值。array_list.zig