概览
结果位置语义(RLS)是驱动 Zig 的零拷贝聚合、类型推断与高效错误传播的“静默引擎”。在附录E体验内联汇编后,我们重新深入编译器,观察 Zig 如何将值直接导向其最终落点。无论是构建结构体、联合体,还是手动填充调用方提供的缓冲,它都能消除临时值。59
Zig 0.15.2 明确了指针对齐与可选结果指针相关的 RLS 诊断,使在构造期间推理数据“居所”更容易。v0.15.2
学习目标
- 跟踪结构体字面量和强制转换如何将结果位置转发到每个字段而不产生隐藏副本。
- 当您想要复用调用方拥有的存储,同时仍提供返回值的API时,应用显式结果指针。
- 使用RLS组合联合体,使每个变体直接写入其自己的有效载荷,而无需在运行时分配临时缓冲。
结构体转发的实践
当你将结构体字面量赋给变量时,Zig 会将该操作重写为一系列字段写入,使各子表达式可继承最终目的地。第一个“菜谱”将若干传感器读数汇总为Report,展示嵌套字面量(Report中的range)如何传递继承结果位置。math.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);
}
$ 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 请注意slice方法接受*const Numbers;从按值参数返回切片会指向临时值并违反安全规则。mem.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]);
}
$ zig test chapters-data/code/60__advanced-result-location-semantics/02_numbers_parse_into.zigAll 2 tests passed.用新值重置Numbers并清零后备数组可确保结果位置准备好复用,即使前一次解析只填充了缓冲区的部分内容。57
联合体变体与分支特定目的地
联合体暴露了相同的机制:一旦编译器知道您正在构造哪个变体,它就将有效载荷的结果位置连接到适当的字段。下面的查找辅助函数要么将字节流式传输到Resource有效载荷中,要么返回格式错误查询的元数据,而不分配中间缓冲区。这种方案可以扩展到流解析器、FFI桥或必须避免堆通信量的缓存。
// ! 演示转发嵌套结果位置的联合体构造。
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.复制到固定大小缓冲时,请如示例所示裁剪长度,以免意外写出有效载荷范围。若需完整保留,请改用切片字段,并配合超出联合值寿命的生命周期管理。10
随手可用的模式
注意与警示
练习
替代方案与边界情况
- 对于
comptime构造,结果位置可能完全存在于编译时内存中;使用@TypeOf来确认您的数据是否会逃逸到运行时。15 - 在对接期望你自行管理缓冲的 C API 时,将 RLS 与
extern结构体结合,以匹配其布局并避免中间拷贝。33 - 在微优化之前分析热路径:有时使用
std.ArrayList或流式写入器会更清晰,而RLS仍会为您消除中间临时值。array_list.zig