概览
第4章介绍了 Zig 的错误联合、try与errdefer的机制;本附录将这些理念转化为速查手册,供你在设计新 API 或重构现有 API 时参考。每个“菜谱”都强化领域错误词汇与最终呈现给用户的诊断消息之间的联系。
Zig 0.15.2 改进了整数转换与分配器失败相关的诊断,使在 Debug 与 ReleaseSafe 构建中依赖精确的错误传播更容易。v0.15.2
学习目标
- 在标准 Zig I/O 失败之上分层特定领域错误集,而不丢失精度。
- 用
errdefer保护堆支持的转换,使每个退出路径都配对分配和释放。 - 将内部错误联合转换为日志和用户界面的清晰、可操作消息。
参考文献:见各章节末尾
分层错误词汇
当某子系统引入其专有错误条件时,请细化错误词汇,而非将一切都塞进anyerror。下述模式将解析失败与模拟 I/O 错误组合为配置特定的联合,使调用方不会混淆NotFound与InvalidPort。4 采用catch |err| switch惯用法可确保映射准确,并与std.fmt.parseInt呈现解析问题的方式保持一致。fmt.zig
// ! 演示在加载配置时分层特定领域的错误集。
const std = @import("std");
pub const ParseError = error{
MissingField,
InvalidPort,
};
pub const SourceError = error{
NotFound,
PermissionDenied,
};
pub const LoadError = SourceError || ParseError;
const SimulatedSource = struct {
payload: ?[]const u8 = null,
failure: ?SourceError = null,
fn fetch(self: SimulatedSource) SourceError![]const u8 {
if (self.failure) |err| return err;
return self.payload orelse SourceError.NotFound;
}
};
fn parsePort(text: []const u8) ParseError!u16 {
var iter = std.mem.splitScalar(u8, text, '=');
const key = iter.next() orelse return ParseError.MissingField;
const value = iter.next() orelse return ParseError.MissingField;
if (!std.mem.eql(u8, key, "PORT")) return ParseError.MissingField;
return std.fmt.parseInt(u16, value, 10) catch ParseError.InvalidPort;
}
pub fn loadPort(source: SimulatedSource) LoadError!u16 {
const line = source.fetch() catch |err| switch (err) {
SourceError.NotFound => return LoadError.NotFound,
SourceError.PermissionDenied => return LoadError.PermissionDenied,
};
return parsePort(line) catch |err| switch (err) {
ParseError.MissingField => return LoadError.MissingField,
ParseError.InvalidPort => return LoadError.InvalidPort,
};
}
test "successful load yields parsed port" {
const source = SimulatedSource{ .payload = "PORT=8080" };
try std.testing.expectEqual(@as(u16, 8080), try loadPort(source));
}
test "parse errors bubble through composed union" {
const source = SimulatedSource{ .payload = "HOST=example" };
try std.testing.expectError(LoadError.MissingField, loadPort(source));
}
test "source failures remain precise" {
const source = SimulatedSource{ .failure = SourceError.PermissionDenied };
try std.testing.expectError(LoadError.PermissionDenied, loadPort(source));
}
$ zig test 01_layered_error_sets.zigAll 3 tests passed.将原始错误名称一直保留到你的 API 边界——调用者可以显式分支到 LoadError.PermissionDenied,这比字符串匹配或哨兵值更稳健。36
用 errdefer 实现均衡清理
字符串组装和 JSON 整形经常分配临时缓冲;当验证步骤失败时忘记释放它们会直接导致泄漏。通过将 std.ArrayListUnmanaged 与 errdefer 配对,下一个菜谱确保成功和失败路径都正确清理,同时仍返回方便的拥有切片。13 这里使用的每个分配辅助函数都在标准库中提供,因此相同结构可扩展到更复杂的构建器。array_list.zig
// ! 展示 errdefer 如何在连接用户代码片段时保持分配平衡。
const std = @import("std");
pub const SnippetError = error{EmptyInput} || std.mem.Allocator.Error;
pub fn joinUpperSnippets(allocator: std.mem.Allocator, parts: []const []const u8) SnippetError![]u8 {
if (parts.len == 0) return SnippetError.EmptyInput;
var list = std.ArrayListUnmanaged(u8){};
errdefer list.deinit(allocator);
for (parts, 0..) |part, index| {
if (index != 0) try list.append(allocator, ' ');
for (part) |ch| try list.append(allocator, std.ascii.toUpper(ch));
}
return list.toOwnedSlice(allocator);
}
test "joinUpperSnippets capitalizes and joins input" {
const allocator = std.testing.allocator;
const result = try joinUpperSnippets(allocator, &[_][]const u8{ "zig", "cookbook" });
defer allocator.free(result);
try std.testing.expectEqualStrings("ZIG COOKBOOK", result);
}
test "joinUpperSnippets surfaces empty-input error" {
const allocator = std.testing.allocator;
try std.testing.expectError(SnippetError.EmptyInput, joinUpperSnippets(allocator, &[_][]const u8{}));
}
$ zig test 02_errdefer_join_upper.zigAll 2 tests passed.由于标准测试分配器会自动抓到泄漏,同时覆盖成功与错误分支可兼作后续编辑的回归保障。13
为用户翻译错误
即使最精心制作的错误集也需要以富有同理心的语言呈现。最后一个模式演示如何为程序化调用者保持原始 ApiError,同时为日志或 UI 文本生成人类可读的叙述。36std.io.fixedBufferStream 使输出对测试具有确定性,专用格式化器将消息传递与控制流隔离。log.zig
// ! 将领域错误桥接到面向用户的日志消息。
const std = @import("std");
pub const ApiError = error{
NotFound,
RateLimited,
Backend,
};
fn describeApiError(err: ApiError, writer: anytype) !void {
switch (err) {
ApiError.NotFound => try writer.writeAll("resource not found; check identifier"),
ApiError.RateLimited => try writer.writeAll("rate limit exceeded; retry later"),
ApiError.Backend => try writer.writeAll("upstream dependency failed; escalate"),
}
}
const Action = struct {
outcomes: []const ?ApiError,
index: usize = 0,
fn invoke(self: *Action) ApiError!void {
if (self.index >= self.outcomes.len) return;
const outcome = self.outcomes[self.index];
self.index += 1;
if (outcome) |err| {
return err;
}
}
};
pub fn runAndReport(action: *Action, writer: anytype) !void {
action.invoke() catch |err| {
try writer.writeAll("Request failed: ");
try describeApiError(err, writer);
return;
};
try writer.writeAll("Request succeeded");
}
test "runAndReport surfaces friendly error message" {
var action = Action{ .outcomes = &[_]?ApiError{ApiError.NotFound} };
var buffer: [128]u8 = undefined;
var stream = std.io.fixedBufferStream(&buffer);
try runAndReport(&action, stream.writer());
const message = stream.getWritten();
try std.testing.expectEqualStrings("Request failed: resource not found; check identifier", message);
}
test "runAndReport acknowledges success" {
var action = Action{ .outcomes = &[_]?ApiError{null} };
var buffer: [64]u8 = undefined;
var stream = std.io.fixedBufferStream(&buffer);
try runAndReport(&action, stream.writer());
const message = stream.getWritten();
try std.testing.expectEqualStrings("Request succeeded", message);
}
$ zig test 03_error_reporting_bridge.zigAll 2 tests passed.保持桥接函数纯净——它只应依赖于错误有效负载和写入器——以便消费者可以交换日志后端或在测试期间在内存中捕获诊断。36
随手可用的模式
注意与警示
- 使用
||合并错误集会保留标签但不保留有效负载数据;若你需要结构化有效负载,应转向带标签的联合体。 - 分配器支持的辅助函数应直接暴露
std.mem.Allocator.Error——调用者期望像标准库容器一样try分配。 - 此处“菜谱”假定在 debug 或 release-safe 构建下使用;在 release-fast 下,你可能需要为本应触发
unreachable的分支添加额外日志。37