概述
C 和 Rust 建立了许多 Zig 开发者带来的心智模型:手动的 malloc/free、RAII 析构函数、Option<T>、Result<T, E> 和特征对象。本附录将这些习惯用法转换为惯用 Zig,使您能够移植真实代码库而不与语言特性对抗。
Zig 收紧的指针对齐规则(@alignCast)和改进的分配器诊断在封装外部 API 时反复出现。v0.15.2
学习目标
- 将手动资源清理替换为
defer/errdefer,同时保持您期望的 C 式控制。 - 以可组合的方式使用 Zig 可选类型和错误联合来表达受 Rust 启发的
Option/Result逻辑。 - 将基于回调或特征的多态性适配到 Zig 的
comptime泛型和指针桩。
转换 C 资源生命周期
C 程序员习惯性地将每个 malloc 与相应的 free 配对。Zig 允许您使用 errdefer 和结构化错误集来编码相同意图,这样即使验证失败缓冲区也永远不会泄漏。4 下面的示例对比了直接转换和自动释放内存的 Zig 优先辅助函数,突出了分配器错误如何与域错误组合。mem.zig
//! Reinvents a C-style buffer duplication with Zig's defer-based cleanup.
const std = @import("std");
pub const NormalizeError = error{InvalidCharacter} || std.mem.Allocator.Error;
pub fn duplicateAlphaUpper(allocator: std.mem.Allocator, input: []const u8) NormalizeError![]u8 {
const buffer = try allocator.alloc(u8, input.len);
errdefer allocator.free(buffer);
for (buffer, input) |*dst, src| switch (src) {
'a'...'z', 'A'...'Z' => dst.* = std.ascii.toUpper(src),
else => return NormalizeError.InvalidCharacter,
};
return buffer;
}
pub fn cStyleDuplicateAlphaUpper(allocator: std.mem.Allocator, input: []const u8) NormalizeError![]u8 {
const buffer = try allocator.alloc(u8, input.len);
var ok = false;
defer if (!ok) allocator.free(buffer);
for (buffer, input) |*dst, src| switch (src) {
'a'...'z', 'A'...'Z' => dst.* = std.ascii.toUpper(src),
else => return NormalizeError.InvalidCharacter,
};
ok = true;
return buffer;
}
test "duplicateAlphaUpper releases buffer on failure" {
const allocator = std.testing.allocator;
try std.testing.expectError(NormalizeError.InvalidCharacter, duplicateAlphaUpper(allocator, "zig-0"));
}
test "c style duplicate succeeds with valid input" {
const allocator = std.testing.allocator;
const dup = try cStyleDuplicateAlphaUpper(allocator, "zig");
defer allocator.free(dup);
try std.testing.expectEqualStrings("ZIG", dup);
}
$ zig test 01_c_style_cleanup.zigAll 2 tests passed.显式的 NormalizeError 联合跟踪分配器失败和验证失败,这种模式在整个 第10章的分配器之旅 中都得到鼓励。
镜像 Rust 的 Option 和 Result 类型
Rust 的 Option<T> 清晰地映射到 Zig 的 ?T,而 Result<T, E> 成为带有丰富标签而非字符串类型消息的错误联合(E!T)。4 这个配方从换行符分隔的文本中提取配置值,首先使用可选搜索,然后使用特定于域的错误联合,将解析失败转换为对调用者友好的诊断。fmt.zig
//! Mirrors Rust's Option and Result idioms with Zig optionals and error unions.
const std = @import("std");
pub fn findPortLine(env: []const u8) ?[]const u8 {
var iter = std.mem.splitScalar(u8, env, '\n');
while (iter.next()) |line| {
if (std.mem.startsWith(u8, line, "PORT=")) {
return line["PORT=".len..];
}
}
return null;
}
pub const ParsePortError = error{
Missing,
Invalid,
};
pub fn parsePort(env: []const u8) ParsePortError!u16 {
const raw = findPortLine(env) orelse return ParsePortError.Missing;
return std.fmt.parseInt(u16, raw, 10) catch ParsePortError.Invalid;
}
test "findPortLine returns optional when key absent" {
try std.testing.expectEqual(@as(?[]const u8, null), findPortLine("HOST=zig-lang"));
}
test "parsePort converts parse errors into domain error set" {
try std.testing.expectEqual(@as(u16, 8080), try parsePort("PORT=8080\n"));
try std.testing.expectError(ParsePortError.Missing, parsePort("HOST=zig"));
try std.testing.expectError(ParsePortError.Invalid, parsePort("PORT=xyz"));
}
$ zig test 02_rust_option_result.zigAll 2 tests passed.因为 Zig 将可选发现与错误传播分离,您可以重用 findPortLine 进行快速路径检查,而 parsePort 处理较慢且可能失败的工作——镜像了将 Option::map 与 Result::map_err 分离的 Rust 模式。17
桥接特征和函数指针
C 和 Rust 都依赖回调——带有上下文有效负载的原始函数指针或具有显式 self 参数的特征对象。Zig 使用 *anyopaque 桩加上 comptime 适配器来建模相同的抽象,这样您可以保持类型安全和零成本间接寻址。33 下面的示例显示了一个 C 风格的回调和类似特征的 handle 方法,通过相同的传统桥重用,依赖于 Zig 的指针转换和对齐断言。builtin.zig
//! Converts a C function-pointer callback pattern into type-safe Zig shims.
const std = @import("std");
pub const LegacyCallback = *const fn (ctx: *anyopaque) void;
fn callLegacy(callback: LegacyCallback, ctx: *anyopaque) void {
callback(ctx);
}
const Counter = struct {
value: u32,
};
fn incrementShim(ctx: *anyopaque) void {
const counter: *Counter = @ptrCast(@alignCast(ctx));
counter.value += 1;
}
pub fn incrementViaLegacy(counter: *Counter) void {
callLegacy(incrementShim, counter);
}
pub fn dispatchWithContext(comptime Handler: type, ctx: *Handler) void {
const shim = struct {
fn invoke(raw: *anyopaque) void {
const typed: *Handler = @ptrCast(@alignCast(raw));
Handler.handle(typed);
}
};
callLegacy(shim.invoke, ctx);
}
const Stats = struct {
total: u32 = 0,
fn handle(self: *Stats) void {
self.total += 2;
}
};
test "incrementViaLegacy integrates with C-style callback" {
var counter = Counter{ .value = 0 };
incrementViaLegacy(&counter);
try std.testing.expectEqual(@as(u32, 1), counter.value);
}
test "dispatchWithContext adapts trait-like handle method" {
var stats = Stats{};
dispatchWithContext(Stats, &stats);
try std.testing.expectEqual(@as(u32, 2), stats.total);
}
$ zig test 03_callback_bridge.zigAll 2 tests passed.额外的 @alignCast 调用反映了 0.15.2 的一个陷阱——指针转换现在断言对齐,因此在封装来自 C 库的 *anyopaque 句柄时将它们保留在原位。v0.15.2