概述
第4章介绍了Zig错误联合、try和errdefer的机制;本附录将这些想法转化为快速参考手册,您可以在起草新API或重构现有API时参考。每个配方都加强了领域特定错误词汇和用户最终看到的诊断消息之间的联系。
Zig 0.15.2改进了整数转换和分配器失败周围的诊断,使得在调试和发布安全构建中更容易依赖精确的错误传播。v0.15.2
学习目标
- 在标准Zig I/O失败之上分层领域特定错误集,而不失去精度。
- 使用
errdefer保护堆支持转换,以便每个退出路径都配对分配和释放。 - 将内部错误联合转换为日志和用户界面的清晰、可操作的消息。
_Refs: _
分层错误词汇
当子系统引入自己的错误条件时,完善词汇而不是将所有东西投入到anyerror。下面的模式从解析失败和模拟I/O错误组合配置特定的联合,以便调用者永远不会失去对NotFound与InvalidPort的追踪。4catch |err| switch习惯用法保持映射诚实,并镜像std.fmt.parseInt如何呈现解析问题。fmt.zig
//! Demonstrates layering domain-specific error sets when loading configuration.
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
//! Shows how errdefer keeps allocations balanced when joining user snippets.
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
//! Bridges domain errors to user-facing log messages.
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分配。 - 这里的配方假设调试或发布安全构建;在发布快速中,您可能希望为否则会触发
unreachable的分支添加额外的日志记录。37