diff --git a/content/learn/coding-in-zig.smd b/content/learn/coding-in-zig.smd index 1001544..ca5cb2a 100644 --- a/content/learn/coding-in-zig.smd +++ b/content/learn/coding-in-zig.smd @@ -20,7 +20,7 @@ const std = @import("std"); pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; const allocator = gpa.allocator(); var lookup = std.StringHashMap(User).init(allocator); @@ -76,7 +76,7 @@ Goku's power is: -1431655766 const std = @import("std"); pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; const allocator = gpa.allocator(); // User -> *const User @@ -115,48 +115,52 @@ const User = struct { const std = @import("std"); const builtin = @import("builtin"); -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const allocator = gpa.allocator(); - - var lookup = std.StringHashMap(User).init(allocator); - defer lookup.deinit(); - - // stdin is an std.io.Reader - // the opposite of an std.io.Writer, which we already saw - const stdin = std.io.getStdIn().reader(); - - // stdout is an std.io.Writer - const stdout = std.io.getStdOut().writer(); - - var i: i32 = 0; - while (true) : (i += 1) { - var buf: [30]u8 = undefined; - try stdout.print("Please enter a name: ", .{}); - if (try stdin.readUntilDelimiterOrEof(&buf, '\n')) |line| { - var name = line; - // Windows 平台换行以 `\r\n` 结束 - // 所以需要截取\r以获取控制台输入字符 - if (builtin.os.tag == .windows) { - name = @constCast(std.mem.trimRight(u8, name, "\r")); - } - - if (name.len == 0) { - break; - } - try lookup.put(name, .{.power = i}); - } - } - - const has_leto = lookup.contains("Leto"); - std.debug.print("{any}\n", .{has_leto}); +pub fn main(init: std.process.Init) !void { + + // 这里的`std.process.Init` 参数是我们之间从来没有见过的 + // 他被称为“juicy main”,是在Zig 0.16版本中添加的,它是完全可选的。 + // 但是它可以省去大多数程序都需要的一些设置样板代码。我在这里使用它来获取一个 + // 默认的`Io`实例(顺便获取一个分配器)。 + // `Io` 不在本教程的讨论范围内。 + const io = init.io; + const allocator = init.gpa; + + var lookup = std.StringHashMap(User).init(allocator); + defer lookup.deinit(); + + var buf: [30]u8 = undefined; + var stdin = std.Io.File.stdin().reader(io, &buf); + + var stdout = std.Io.File.stdout().writer(io, &.{}); + + var i: i32 = 0; + while (true) : (i += 1) { + try stdout.interface.print("Please enter a name: ", .{}); + var name = try stdin.interface.takeDelimiter('\n') orelse break; + if (builtin.os.tag == .windows) { + // In Windows lines are terminated by \r\n. + // We need to strip out the \r + name = @constCast(std.mem.trimEnd(u8, name, "\r")); + } + if (name.len == 0) { + break; + } + try lookup.put(name, .{.power = i}); + } + + const has_leto = lookup.contains("Leto"); + std.debug.print("{any}\n", .{has_leto}); } const User = struct { - power: i32, + power: i32, }; ``` +> 这段代码的初始版本在Windows上无法编译成功。因此,需要添加你现在看到的`@constCast`。我们之前也见过其他的内置函数,但是这个函数更加高级一些。我曾考虑删除整行代码,但考虑到Windows用户也能轻松上周,所以还是保留了`@constCast`。虽然这种情况有更加简单的解决方案,但是我最终还是决定使用不安全的`@constCast`。我根据这个例子的情况写了[一篇博客文章](https://www.openmymind.net/Zigs-ConstCast/)来解释这样做的必要性--但是这篇文章要高级的多。建议你在深入学习zig后再来阅读这篇文章。 + + + 上述代码虽然区分大小写,但无论我们如何完美地输入 `Leto`,`contains` 总是返回 `false`。让我们通过遍历 `lookup` 打印其值来调试一下: ```zig @@ -226,64 +230,60 @@ defer { ```zig const std = @import("std"); -const Allocator = std.mem.Allocator; const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const allocator = gpa.allocator(); - - var arr = std.ArrayList(User).init(allocator); - defer { - for (arr.items) |user| { - user.deinit(allocator); - } - arr.deinit(); - } - - // stdin is an std.io.Reader - // the opposite of an std.io.Writer, which we already saw - const stdin = std.io.getStdIn().reader(); - - // stdout is an std.io.Writer - const stdout = std.io.getStdOut().writer(); - - var i: i32 = 0; - while (true) : (i += 1) { - var buf: [30]u8 = undefined; - try stdout.print("Please enter a name: ", .{}); - if (try stdin.readUntilDelimiterOrEof(&buf, '\n')) |line| { - var name = line; - if (builtin.os.tag == .windows) { - name = @constCast(std.mem.trimRight(u8, name, "\r")); - } - - if (name.len == 0) { - break; - } - const owned_name = try allocator.dupe(u8, name); - try arr.append(.{.name = owned_name, .power = i}); - } - } - - var has_leto = false; - for (arr.items) |user| { - if (std.mem.eql(u8, "Leto", user.name)) { - has_leto = true; - break; - } - } - - std.debug.print("{any}\n", .{has_leto}); +pub fn main(init: std.process.Init) !void { + const io = init.io; + const allocator = init.gpa; + + var arr: std.ArrayList(User) = .empty; + defer { + for (arr.items) |user| { + user.deinit(allocator); + } + arr.deinit(allocator); + } + + var buf: [30]u8 = undefined; + var stdin = std.Io.File.stdin().reader(io, &buf); + var stdout = std.Io.File.stdout().writer(io, &.{}); + + var i: i32 = 0; + while (true) : (i += 1) { + try stdout.interface.print("Please enter a name: ", .{}); + var name = try stdin.interface.takeDelimiter('\n') orelse break; + + if (builtin.os.tag == .windows) { + // In Windows lines are terminated by \r\n. + // We need to strip out the \r + name = @constCast(std.mem.trimEnd(u8, name, "\r")); + } + if (name.len == 0) { + break; + } + const owned_name = try allocator.dupe(u8, name); + try arr.append(allocator, .{.name = owned_name, .power = i}); + } + + var has_leto = false; + for (arr.items) |user| { + if (std.mem.eql(u8, "Leto", user.name)) { + has_leto = true; + break; + } + } + + std.debug.print("{any}\n", .{has_leto}); } const User = struct { - name: []const u8, - power: i32, + name: []const u8, + power: i32, - fn deinit(self: User, allocator: Allocator) void { - allocator.free(self.name); - } + fn deinit(self: User, allocator: Allocator) void { + allocator.free(self.name); + } }; ``` @@ -297,8 +297,8 @@ const User = struct { const std = @import("std"); pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const allocator = gpa.allocator(); + var dpa = std.heap.DebugAllocator(.{}){}; + const allocator = dpa.allocator(); var out = std.ArrayList(u8).init(allocator); defer out.deinit(); @@ -351,37 +351,31 @@ try l.info("sever started", true); no field or member function named 'writeAll' in 'bool' ``` -使用 `ArrayList(u8)` 的 `writer` 就可以运行: +但是如果我们传递一个实现了`writeAll`函数的`*std.Io.Writer`对象,代码就能正常运行 ```zig pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const allocator = gpa.allocator(); + var l = Logger{.level = .info}; - var l = Logger{.level = .info}; - - var arr = std.ArrayList(u8).init(allocator); - defer arr.deinit(); - - try l.info("sever started", arr.writer()); - std.debug.print("{s}\n", .{arr.items}); + const w = LineLimiter{.max = 80}; + try l.info("a" ** 85, &w); } -``` - -`anytype` 的一个最大缺点就是文档。下面是我们用过几次的 `std.json.stringify` 函数的签名: -```zig -// 我**讨厌**多行函数定义 -// 不过,鉴于你可能在小屏幕上阅读这个指南,因此这里破一次例。 - -fn stringify( - value: anytype, - options: StringifyOptions, - out_stream: anytype -) @TypeOf(out_stream).Error!void +const LineLimiter = struct { + max: usize, + + pub fn writeAll(self: *const LineLimiter, data: []const u8) !void { + var remaining = data; + while (remaining.len > 0) { + const len = @min(self.max, remaining.len); + std.debug.print("{s}\n", .{remaining[0..len]}); + remaining = remaining[len..]; + } + } +}; ``` -第一个参数 `value: anytype` 是显而易见的,它是要序列化的值,可以是任何类型(实际上,Zig 的 JSON 序列化器不能序列化某些类型,比如 HashMap)。我们可以猜测,`out_stream` 是写入 JSON 的地方,但至于它需要实现什么方法,你和我一样猜得到。唯一的办法就是阅读源代码,或者传递一个假值,然后使用编译器错误作为我们的文档。如果有更好的自动文档生成器,这一点可能会得到改善。不过,我希望 Zig 能提供接口,这已经不是第一次了。 +在这种情况下,LineLimiter最好实现`*std.Io.Writer`接口,一边更通用的使用。但是在Zig 0.15版本-该版本重新设计了Writer和Reader接口之前,使用anytype作为通用写入器是很常见的做法。虽然这种用法可能会被逐步淘汰,但你一定会看到并以大致相同的方式使用`anytype`;只是可能不太用于捕获不同的写入器。 ## [@TypeOf]($heading.id('typeof')) @@ -473,16 +467,37 @@ run_step.dependOn(&run_cmd.step); 要添加『测试』步骤,你需要重复刚才添加的大部分运行代码,只是不再使用 `b.addExecutable`,而是使用 `b.addTest`: ```zig -const tests = b.addTest(.{ +const mod = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("learning.zig"), }); -const test_cmd = b.addRunArtifact(tests); -test_cmd.step.dependOn(b.getInstallStep()); -const test_step = b.step("test", "Run the tests"); -test_step.dependOn(&test_cmd.step); +{ + // setup exe + const exe = b.addExecutable(.{ + .name = "learning", + .root_module = mod, + }); + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + const run_step = b.step("run", "Start learning!"); + run_step.dependOn(&run_cmd.step); +} + +{ + // setup tests + const tests = b.addTest(.{ + .root_module = mod, + }); + + const test_cmd = b.addRunArtifact(tests); + test_cmd.step.dependOn(b.getInstallStep()); + const test_step = b.step("test", "Run the tests"); + test_step.dependOn(&test_cmd.step); +} ``` 我们将该步骤命名为 `test`。运行 `zig build --help` 会显示另一个可用步骤 `test`。由于我们没有进行任何测试,因此很难判断这一步是否有效。在 `learning.zig` 中,添加 @@ -574,45 +589,20 @@ pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - const calc_module = b.addModule("calc", .{ + const calc_module = b.createModule("calc", .{ .root_source_file = b.path("PATH_TO_CALC_PROJECT/calc.zig"), }); - { - // 设置我们的 "run" 命令。 - - const exe = b.addExecutable(.{ - .name = "learning", - .target = target, - .optimize = optimize, - .root_source_file = b.path("learning.zig"), - }); - // 添加这些代码 - exe.root_module.addImport("calc", calc_module); - b.installArtifact(exe); - - const run_cmd = b.addRunArtifact(exe); - run_cmd.step.dependOn(b.getInstallStep()); - - const run_step = b.step("run", "Start learning!"); - run_step.dependOn(&run_cmd.step); - } + const mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .root_source_file = b.path("learning.zig"), + .imports = &.{ + .{.name = "calc", .module = calc_module}, + } + }); - { - // 设置我们的 "test" 命令。 - const tests = b.addTest(.{ - .target = target, - .optimize = optimize, - .root_source_file = b.path("learning.zig"), - }); - // 添加这行代码 - tests.root_module.addImport("calc", calc_module); - - const test_cmd = b.addRunArtifact(tests); - test_cmd.step.dependOn(b.getInstallStep()); - const test_step = b.step("test", "Run the tests"); - test_step.dependOn(&test_cmd.step); - } + // setup our exe and test like before } ``` diff --git a/content/learn/generics.smd b/content/learn/generics.smd index 6754604..bd3c9e9 100644 --- a/content/learn/generics.smd +++ b/content/learn/generics.smd @@ -119,7 +119,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; const allocator = gpa.allocator(); var list = try List(u32).init(allocator); diff --git a/content/learn/heap-memory.smd b/content/learn/heap-memory.smd index 69513df..c470669 100644 --- a/content/learn/heap-memory.smd +++ b/content/learn/heap-memory.smd @@ -27,7 +27,7 @@ const std = @import("std"); pub fn main() !void { // we'll be talking about allocators shortly - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; const allocator = gpa.allocator(); // ** The next two lines are the important ones ** @@ -41,10 +41,10 @@ pub fn main() !void { } fn getRandomCount() !u8 { - var seed: u64 = undefined; - try std.posix.getrandom(std.mem.asBytes(&seed)); - var random = std.Random.DefaultPrng.init(seed); - return random.random().uintAtMost(u8, 5) + 5; + // TODO: use a proper seed, not 0. + var prng = std.Random.DefaultPrng.init(0); + const random = prng.random(); + return random.uintAtMost(u8, 5) + 5; } ``` @@ -78,6 +78,12 @@ pub const Game = struct { // store 10 most recent moves per player var history = try allocator.alloc(Move, player_count * 10); + // This isn't strickly necessary. Nothing following the above alloc + // can fail, so this errdefer will never be executed. HOWEVER, if the + // function changes and fallible code is added, it could be easy to forget + // adding this. + errdefer allocator.free(history); + return .{ .players = players, .history = history, @@ -110,7 +116,7 @@ pub const Game = struct { const std = @import("std"); pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; const allocator = gpa.allocator(); var arr = try allocator.alloc(usize, 4); @@ -170,7 +176,7 @@ const std = @import("std"); pub fn main() !void { // again, we'll talk about allocators soon! - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; const allocator = gpa.allocator(); // create a User on the heap @@ -248,15 +254,16 @@ defer allocator.free(say); 另一种形式是将 `Allocator` 传递给 `init` ,然后由对象**内部使用**。这种方法不那么明确,因为你已经给了对象一个分配器来使用,但你不知道哪些方法调用将实际分配。对于长寿命对象来说,这种方法更实用。 -注入分配器的优势不仅在于显式,还在于灵活性。`std.mem.Allocator` 是一个接口,提供了 `alloc`、`free`、`create` 和 `destroy` 函数以及其他一些函数。到目前为止,我们只看到了 `std.heap.GeneralPurposeAllocator`,但标准库或第三方库中还有其他实现。 +注入分配器的优势不仅在于显式,还在于灵活性。`std.mem.Allocator` 是一个接口,提供了 `alloc`、`free`、`create` 和 `destroy` 函数以及其他一些函数。到目前为止,我们只看到了 `std.heap.DebugAllocator`,但标准库或第三方库中还有其他实现。 > Zig 没有用于创建接口的语法糖。一种类似于接口的模式是带标签的联合(tagged unions),不过与真正的接口相比,这种模式相对受限。整个标准库中也探索了一些其他模式,例如 `std.mem.Allocator`。本指南不探讨这些接口模式。 如果你正在构建一个库,那么最好接受一个 `std.mem.Allocator`,然后让库的用户决定使用哪种分配器实现。否则,你就需要选择正确的分配器,正如我们将看到的,这些分配器并不相互排斥。在你的程序中创建不同的分配器可能有很好的理由。 -## [通用分配器 GeneralPurposeAllocator]($heading.id('general-purpose-allocator')) +## [调试分配器 DebugAllocator]($heading.id('debug-allocator')) + +顾名思义,`std.heap.DebugAllocator` 是一种在开发过程中使用的线程安全的分配器。对于很多程序而已,这也将会是他们魏需要的分配器。在程序启动时,会创建一个分配器并传递给需要它的函数。我的 HTTP 服务器库中的示例代码就是一个很好的例子: -顾名思义,`std.heap.GeneralPurposeAllocator` 是一种通用的、线程安全的分配器,可以作为应用程序的主分配器。对于许多程序来说,这是唯一需要的分配器。程序启动时,会创建一个分配器并传递给需要它的函数。我的 HTTP 服务器库中的示例代码就是一个很好的例子: ```zig const std = @import("std"); @@ -264,7 +271,7 @@ const httpz = @import("httpz"); pub fn main() !void { // create our general purpose allocator - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; // get an std.mem.Allocator from it const allocator = gpa.allocator(); @@ -280,28 +287,36 @@ pub fn main() !void { } ``` -我们创建了 `GeneralPurposeAllocator`,从中获取一个 `std.mem.Allocator` 并将其传递给 HTTP 服务器的 `init` 函数。在一个更复杂的项目中,声明的变量`allocator` 可能会被传递给代码的多个部分,每个部分可能都会将其传递给自己的函数、对象和依赖。 +我们创建了 `DebugAllocator`,从中获取一个 `std.mem.Allocator` 并将其传递给 HTTP 服务器的 `init` 函数。在一个更复杂的项目中,声明的变量`allocator` 可能会被传递给代码的多个部分,每个部分可能都会将其传递给自己的函数、对象和依赖。 -你可能会注意到,创建 `gpa` 的语法有点奇怪。什么是`GeneralPurposeAllocator(.{}){}`? +你可能会注意到,创建 `gpa` 的语法有点奇怪。什么是`DebugAllocator(.{}){}`? -我们之前见过这些东西,只是现在都混合了起来。`std.heap.GeneralPurposeAllocator` 是一个函数,由于它使用的是 `PascalCase`(帕斯卡命名法),我们知道它返回一个类型。(下一部分会更多讨论泛型)。也许这个更明确的版本会更容易解读: +我们之前见过这些东西,只是现在都混合了起来。`std.heap.DebugAllocator` 是一个函数,由于它使用的是 `PascalCase`(帕斯卡命名法),我们知道它返回一个类型。(下一部分会更多讨论泛型)。也许这个更明确的版本会更容易解读: ```zig -const T = std.heap.GeneralPurposeAllocator(.{}); +const T = std.heap.DebugAllocator(.{}); var gpa = T{}; -// 等同于: +// is the same as: -var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +var gpa = std.heap.DebugAllocator(.{}){}; ``` 也许你仍然不太确信 `.{}` 的含义。我们之前也见过它:`.{}` 是一个具有隐式类型的结构体初始化器。 类型是什么,字段在哪里?类型其实是 `std.heap.general_purpose_allocator.Config`,但它并没有直接暴露出来,这也是我们没有显式给出类型的原因之一。没有设置字段是因为 Config 结构定义了默认值,我们将使用默认值。这是配置、选项的中常见的模式。事实上,我们在下面几行向 `init` 传递 `.{.port = 5882}` 时又看到了这种情况。在本例中,除了端口这一个字段外,我们都使用了默认值。 +如果这种写法还是不够清晰,Zig最近引入了一种叫做声明字面量(declaration literals)的功能。虽然这个功能并不复杂,我们也不会展示他的具体工作原理,但是一下是如果用于提高分配器初始化代码的可读性的展示: + +```zig +var gpa: std.heap.DebugAllocator(.{}) = .init; +``` + +请注意,`init` 方法仅适用于 `DebugAllocator`。其他使用声明字面量的类型可能具有不同的(或多个不同的)名称。例如,空哈希映射使用 `.empty` 声明字面量进行初始化。 + ## [std.testing.allocator]($heading.id('std-testing-allocator')) -希望当我们谈到内存泄漏时,你已经足够烦恼,而当我提到 Zig 可以提供帮助时,你肯定渴望了解更多这方面内容。这种帮助来自 `std.testing.allocator`,它是一个 `std.mem.Allocator` 实现。目前,它基于通用分配器(GeneralPurposeAllocator)实现,并与 Zig 的测试运行器进行了集成,但这只是实现细节。重要的是,如果我们在测试中使用 `std.testing.allocator`,就能捕捉到大部分内存泄漏。 +希望当我们谈到内存泄漏时,你已经足够烦恼,而当我提到 Zig 可以提供帮助时,你肯定渴望了解更多这方面内容。这种帮助来自 `std.testing.allocator`,它是一个 `std.mem.Allocator` 实现。目前,它基于通用分配器(DebugAllocator)实现,并与 Zig 的测试运行器进行了集成,但这只是实现细节。重要的是,如果我们在测试中使用 `std.testing.allocator`,就能捕捉到大部分内存泄漏。 你可能已经熟悉了动态数组(通常称为 ArrayLists)。在许多动态编程语言中,所有数组都是动态的。动态数组支持可变数量的元素。Zig 有一个通用 ArrayList,但我们将创建一个专门用于保存整数的 ArrayList,并演示泄漏检测: @@ -351,7 +366,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; const allocator = gpa.allocator(); var list = try IntList.init(allocator); @@ -378,24 +393,22 @@ test "IntList: add" { try list.add(@intCast(i+10)); } - try testing.expectEqual(@as(usize, 5), list.pos); - try testing.expectEqual(@as(i64, 10), list.items[0]); - try testing.expectEqual(@as(i64, 11), list.items[1]); - try testing.expectEqual(@as(i64, 12), list.items[2]); - try testing.expectEqual(@as(i64, 13), list.items[3]); - try testing.expectEqual(@as(i64, 14), list.items[4]); + try testing.expectEqual(5, list.pos); + try testing.expectEqual(10, list.items[0]); + try testing.expectEqual(11, list.items[1]); + try testing.expectEqual(12, list.items[2]); + try testing.expectEqual(13, list.items[3]); + try testing.expectEqual(14, list.items[4]); } ``` -> `@as` 是一个执行类型强制的内置函数。如果你好奇什么我们的测试要用到这么多,那么你不是唯一一个。从技术上讲,这是因为第二个参数,即 `actual`,被强制为第一个参数,即 `expected`。在上面的例子中,我们的期望值都是 `comptime_int`,这就造成了问题。包括我在内的许多人都认为这是一种奇怪而不幸的行为。 - 如果你按照步骤操作,把测试放在 `IntList` 和 `main` 的同一个文件中。Zig 的测试通常写在同一个文件中,经常在它们测试的代码附近。当使用 `zig test learning.zig` 运行测试时,我们会得到了一个令人惊喜的失败: ```bash Test [1/1] test.IntList: add... [gpa] (err): memory address 0x101154000 leaked: /code/zig/learning.zig:26:32: 0x100f707b7 in init (test) - .items = try allocator.alloc(i64, 2), - ^ + .items = try allocator.alloc(i64, 2), + ^ /code/zig/learning.zig:55:29: 0x100f711df in test.IntList: add (test) var list = try IntList.init(testing.allocator); @@ -403,10 +416,10 @@ Test [1/1] test.IntList: add... [gpa] (err): memory address 0x101154000 leaked: [gpa] (err): memory address 0x101184000 leaked: /code/test/learning.zig:40:41: 0x100f70c73 in add (test) - var larger = try self.allocator.alloc(i64, len * 2); - ^ + var larger = try self.allocator.alloc(i64, len * 2); + ^ /code/test/learning.zig:59:15: 0x100f7130f in test.IntList: add (test) - try list.add(@intCast(i+10)); + try list.add(@intCast(i+10)); ``` 此处有多个内存泄漏。幸运的是,测试分配器准确地告诉我们泄漏的内存是在哪里分配的。你现在能发现泄漏了吗?如果没有,请记住,通常情况下,每个 `alloc` 都应该有一个相应的 `free`。我们的代码在 `deinit` 中调用 `free` 一次。然而在 `init` 中 `alloc` 被调用一次,每次调用 `add` 并需要更多空间时也会调用 `alloc`。每次我们 `alloc` 更多空间时,都需要 `free` 之前的 `self.items`。 @@ -425,16 +438,20 @@ self.allocator.free(self.items); ## [ArenaAllocator]($heading.id('arena-allocator')) -通用分配器(GeneralPurposeAllocator)是一个合理的默认设置,因为它在所有可能的情况下都能很好地工作。但在程序中,你可能会遇到一些固定场景,使用更专业的分配器可能会更合适。其中一个例子就是需要在处理完成后丢弃的短期状态。解析器(Parser)通常就有这样的需求。一个 `parse` 函数的基本轮廓可能是这样的 +调试分配器(DebugAllocator)是一个合理的开发调试设置,因为它在所有可能的情况下都能很好地工作。但在程序中,你可能会遇到一些固定场景,使用更专业的分配器可能会更合适。其中一个例子就是需要在处理完成后丢弃的短期状态。解析器(Parser)通常就有这样的需求。一个 `parse` 函数的基本轮廓可能是这样的 ```zig fn parse(allocator: Allocator, input: []const u8) !Something { + const buf = try allocator.alloc(u8, 512); + defer allocator.free(buf); + + const nesting = try allocator.alloc(NestType, 10) + defer allocator.free(nesting); + const state = State{ - .buf = try allocator.alloc(u8, 512), - .nesting = try allocator.alloc(NestType, 10), + .buf = buf, + .nesting = nesting, }; - defer allocator.free(state.buf); - defer allocator.free(state.nesting); return parseInternal(allocator, state, input); } @@ -477,7 +494,7 @@ fn parse(allocator: Allocator, input: []const u8) !Something { 我们的 `IntList` 却不是这样。它可以用来存储 10 个或 1000 万个值。它的生命周期可以以毫秒为单位,也可以以周为单位。它无法决定使用哪种类型的分配器。使用 IntList 的代码才有这种知识。最初,我们是这样管理 `IntList` 的: ```zig -var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +var gpa = std.heap.DebugAllocator(.{}){}; const allocator = gpa.allocator(); var list = try IntList.init(allocator); @@ -487,7 +504,7 @@ defer list.deinit(); 我们可以选择 `ArenaAllocator` 替代: ```zig -var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +var gpa = std.heap.DebugAllocator(.{}){}; const allocator = gpa.allocator(); var arena = std.heap.ArenaAllocator.init(allocator); @@ -496,9 +513,8 @@ const aa = arena.allocator(); var list = try IntList.init(aa); -// 说实话,我很纠结是否应该调用 list.deinit。 -// 从技术上讲,我们不必这样做,因为我们在上面调用了 defer arena.deinit()。 - +// I'm honestly torn on whether or not we should call list.deinit. +// Technically, we don't have to since we call defer arena.deinit() above. defer list.deinit(); ... @@ -554,6 +570,31 @@ pub fn main() !void { 固定缓冲区分配器(FixedBufferAllocators)的常见模式是 `reset` 并重复使用,竞技场分配器(ArenaAllocators)也是如此。这将释放所有先前的分配,并允许重新使用分配器。 +## Non-Debug Allocator + +自从我提到 DebugAllocator 以来,您可能一直在想是否存在非“调试”版本的替代方案?您完全可以在发布版本中使用 DebugAllocator,只是它无法提供某些系统所需的顶级性能。不过,我认为这种好奇心是合理的,所以这里列举一个您在 Zig 应用程序中常见的模式: + +```zig +const std = @import("std"); +const builtin = @import("builtin"); + +pub fn main() !void { + var gpa: std.heap.DebugAllocator(.{}) = .empty; + const allocator = if (builtin.mode == .Debug) gpa.allocator() + else std.heap.c_allocator; + + defer if (builtin.mode == .Debug) { + if (gpa.detectLeaks()) { + std.posix.exit(1); + } + }; + + // ... +} +``` + +我们不会详细讨论这一点,但简而言之,我们在调试版本中使用 DebugAllocator,在发布版本中使用 std.heap.c_allocator。std.heap.c_allocator 是 malloc 的一个封装。另一个值得注意的改进是,在调试模式下,程序退出时,我们会利用 DebugAllocator 的内存泄漏检测和报告功能。 + --- 由于没有默认的分配器,Zig 在分配方面既透明又灵活。`std.mem.Allocator`接口非常强大,它允许专门的分配器封装更通用的分配器,正如我们在`ArenaAllocator`中看到的那样。 @@ -581,15 +622,27 @@ pub fn main() !void { ```zig const std = @import("std"); - pub fn main() !void { - const out = std.io.getStdOut(); + // Zig implementation of Write (and Reader) started to change drastically in + // 0.15 and even more in 0.16. + // The new approach has a number of benefits, but simplicity isn't one of them. - try std.json.stringify(.{ - .this_is = "an anonymous struct", - .above = true, - .last_param = "are options", - }, .{.whitespace = .indent_2}, out.writer()); + var gpa = std.heap.DebugAllocator(.{}){}; + const allocator = gpa.allocator(); + + var buf = std.Io.Writer.Allocating.init(allocator); + defer buf.deinit(); + + try dump(&buf.writer); + std.debug.print("{s}\n", .{buf.written()}); +} + +fn dump(writer: *std.Io.Writer) !void { + try std.json.Stringify.value(.{ + .this_is = "an anonymous struct", + .above = true, + .last_param = "are options", + }, .{.whitespace = .indent_2}, writer); } ``` @@ -597,4 +650,4 @@ pub fn main() !void { 在很多情况下,用 `std.io.BufferedWriter` 封装我们的 `Writer` 会大大提高性能。 -我们的目标并不是消除所有动态分配。这行不通,因为这些替代方案只有在特定情况下才有意义。但现在你有了很多选择。从堆栈到通用分配器,以及所有介于两者之间的东西,比如静态缓冲区、流式 `Writer` 和专用分配器。 +我们的目标并不是消除所有动态分配。这行不通,因为这些替代方案只有在特定情况下才有意义。但现在你有了很多选择。从堆栈到调试分配器,以及所有介于两者之间的东西,比如静态缓冲区、流式 `Writer` 和专用分配器。