Chapter 46Io And Stream Adapters

I/O和流适配器

概述

前一章专注于格式化和文本,而其他章节介绍了使用简单缓冲输出的基本打印。本章深入探讨Zig 0.15.2的流式原语:现代std.Io.Reader/std.Io.Writer接口及其支持适配器(有限视图、丢弃、复制、简单计数)。这些抽象有意暴露缓冲区内部,以便性能关键路径(格式化、分隔符扫描、哈希)保持确定性和无分配。与其他语言中发现的不透明I/O层不同,Zig的适配器超薄——通常是简单结构,其方法操作显式切片和索引。Writer.zigReader.zig

您将学习如何创建固定内存写入器、迁移遗留std.io.fixedBufferStream使用、用limited限制读取、复制输入流(tee)、高效丢弃输出,以及组装管道(例如,分隔符处理)而不需要隐藏分配。每个示例都很小、自包含,并演示了一个概念,您可以在连接到文件、套接字或未来异步抽象时重用该概念。

学习目标

  • 使用 Writer.fixed / Reader.fixed 构建固定缓冲区写入器/读取器并检查缓冲数据。
  • 安全地从遗留 std.io.fixedBufferStream 迁移到更新的API。44
  • 使用 Reader.limited 强制执行字节限制,以保护解析器免受失控输入的影响。Limited.zig
  • 实现复制(tee)和丢弃模式,而无需额外的分配。10
  • 使用 takeDelimiter / 相关辅助函数进行分隔符分隔数据的流式传输,用于行处理。
  • 推理何时选择缓冲与直接流式传输及其性能影响。39

基础:固定写入器和读取器

核心抽象是表示流端点状态的值类型。固定写入器会缓冲字节,直到满或刷新。固定读取器公开其缓冲区域的切片并提供peek/take语义,促进增量解析而无需复制。3

基本固定写入器 ()

创建内存写入器,发出格式化内容,然后检查并转发缓冲切片。这反映了早期的格式化模式,但无需分配 ArrayList 或处理动态容量。45

Zig
const std = @import("std");

// Demonstrates basic buffered writing using the new std.Io.Writer API
// and then flushing to stdout via the older std.io File writer.
pub fn main() !void {
    var buf: [128]u8 = undefined;
    // New streaming Writer backed by a fixed buffer. Writes accumulate until flushed/consumed.
    var w: std.Io.Writer = .fixed(&buf);

    try w.print("Header: {s}\n", .{"I/O adapters"});
    try w.print("Value A: {d}\n", .{42});
    try w.print("Value B: {x}\n", .{0xdeadbeef});

    // Grab buffered bytes and print through std.debug (stdout)
    const buffered = w.buffered();
    std.debug.print("{s}", .{buffered});
}
Run
Shell
$ zig run reader_writer_basics.zig
输出
Shell
Header: I/O adapters
Value A: 42
Value B: deadbeef

缓冲区由用户所有;您决定其生命周期和大小预算。不会发生隐式堆分配——这对于紧循环或嵌入式目标至关重要。

从 迁移

遗留 fixedBufferStream(小写 io)返回具有 reader() / writer() 方法的包装器类型。Zig 0.15.2保留它们以保持兼容性,但更喜欢使用 std.Io.Writer.fixed / Reader.fixed 进行统一的适配器组合。1fixed_buffer_stream.zig

Zig
const std = @import("std");

// Demonstrates legacy fixedBufferStream (deprecated in favor of std.Io.Writer.fixed)
// to highlight migration paths.
pub fn main() !void {
    var backing: [64]u8 = undefined;
    var fbs = std.io.fixedBufferStream(&backing);
    const w = fbs.writer();

    try w.print("Legacy buffered writer example: {s} {d}\n", .{ "answer", 42 });
    try w.print("Capacity used: {d}/{d}\n", .{ fbs.getWritten().len, backing.len });

    // Echo buffer contents to stdout.
    std.debug.print("{s}", .{fbs.getWritten()});
}
Run
Shell
$ zig run fixed_buffer_stream.zig
输出
Shell
Legacy buffered writer example: answer 42
Capacity used: 42/64

为未来的互操作性更喜欢新的首字母大写 Io API;随着更多适配器以现代接口为目标,fixedBufferStream 最终可能会逐步淘汰。

限制输入 ()

用硬上限包装读取器以防御过大的输入(例如,标题部分、魔术前缀)。一旦限制耗尽,后续读取会提前指示流结束,保护下游逻辑。4

Zig
const std = @import("std");

// Reads at most N bytes from an input using std.Io.Reader.Limited
pub fn main() !void {
    const input = "Hello, world!\nRest is skipped";
    var r: std.Io.Reader = .fixed(input);

    var tmp: [8]u8 = undefined; // buffer backing the limited reader
    var limited = r.limited(.limited(5), &tmp); // allow only first 5 bytes

    var out_buf: [64]u8 = undefined;
    var out: std.Io.Writer = .fixed(&out_buf);

    // Pump until limit triggers EndOfStream for the limited reader
    _ = limited.interface.streamRemaining(&out) catch |err| {
        switch (err) {
            error.WriteFailed, error.ReadFailed => unreachable,
        }
    };

    std.debug.print("{s}\n", .{out.buffered()});
}
Run
Shell
$ zig run limited_reader.zig
Output
Shell
Hello

使用 limited(.limited(N), tmp_buffer) 进行协议保护;解析函数可以假设有界消耗并在提前结束时干净地退出。33

适配器与模式

更高级别的行为(计数、tee、丢弃、分隔符流式传输)来自对 buffered() 的简单循环和小辅助函数,而不是繁重的继承或特征链。39

字节计数(缓冲长度)

在许多场景中,您只需要到目前为止产生的字节数——读取写入器当前缓冲切片的就足够了,避免了专用计数适配器。10

Zig
const std = @import("std");

// Simple counting example using Writer.fixed and buffered length.
pub fn main() !void {
    var buf: [128]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    try w.print("Counting: {s} {d}\n", .{"bytes", 123});
    try w.print("And more\n", .{});
    const written = w.buffered().len;
    std.debug.print("Total bytes logically written: {d}\n", .{written});
}
Run
Shell
$ zig run counting_writer.zig
输出
Shell
Total bytes logically written: 29

对于在刷新后缓冲区长度重置的流式接收器,集成自定义 update 函数(参见哈希写入器设计)以跨刷新边界累积总计。

丢弃输出 ()

基准测试和干运行通常需要测量格式化或转换成本,而不保留结果。消费缓冲区会将其长度清零;后续写入继续正常进行。45

Zig
const std = @import("std");

// Demonstrate std.Io.Writer.Discarding to ignore outputs (useful in benchmarks)
pub fn main() !void {
    var buf: [32]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);

    try w.print("Ephemeral output: {d}\n", .{999});

    // Discard content by consuming buffered bytes
    _ = std.Io.Writer.consumeAll(&w);

    // Show buffer now empty
    std.debug.print("Buffer after consumeAll length: {d}\n", .{w.buffered().len});
}
Run
Shell
$ zig run discarding_writer.zig
Output
Shell
Buffer after consumeAll length: 0

consumeAll 是一种结构性的无分配操作;它只是调整 end 并在需要时移动剩余字节。对于紧密内循环来说足够便宜。

Tee / 复制

复制流("tee")可以手动构建:peek,写入两个目标,丢弃。这避免了中间堆缓冲区,适用于有限或流水线输入。28

Zig
const std = @import("std");

fn tee(r: *std.Io.Reader, a: *std.Io.Writer, b: *std.Io.Writer) !void {
    while (true) {
        const chunk = r.peekGreedy(1) catch |err| switch (err) {
            error.EndOfStream => break,
            error.ReadFailed => return err,
        };
        try a.writeAll(chunk);
        try b.writeAll(chunk);
        r.toss(chunk.len);
    }
}

pub fn main() !void {
    const input = "tee me please";
    var r: std.Io.Reader = .fixed(input);

    var abuf: [64]u8 = undefined;
    var bbuf: [64]u8 = undefined;
    var a: std.Io.Writer = .fixed(&abuf);
    var b: std.Io.Writer = .fixed(&bbuf);

    try tee(&r, &a, &b);

    std.debug.print("A: {s}\nB: {s}\n", .{ a.buffered(), b.buffered() });
}
Run
Shell
$ zig run tee_stream.zig
Output
Shell
A: tee me please
B: tee me please

始终在写入前使用 peekGreedy(1)(或适当大小);未能确保缓冲内容可能导致不必要的底层读取或过早终止。44

分隔符流式传输管道

基于行或记录的协议受益于 takeDelimiter,它返回不包括分隔符的切片。循环直到 null 来处理所有逻辑行,而无需复制或分配。31

Zig
const std = @import("std");

// Demonstrates composing Reader -> Writer pipeline with delimiter streaming.
pub fn main() !void {
    const data = "alpha\nbeta\ngamma\n";
    var r: std.Io.Reader = .fixed(data);

    var out_buf: [128]u8 = undefined;
    var out: std.Io.Writer = .fixed(&out_buf);

    while (true) {
        // Stream one line (excluding the delimiter) then print processed form
        const line_opt = r.takeDelimiter('\n') catch |err| switch (err) {
            error.StreamTooLong => unreachable,
            error.ReadFailed => return err,
        };
        if (line_opt) |line| {
            try out.print("Line({d}): {s}\n", .{ line.len, line });
        } else break;
    }

    std.debug.print("{s}", .{out.buffered()});
}
Run
Shell
$ zig run stream_pipeline.zig
Output
Shell
Line(5): alpha
Line(4): beta
Line(5): gamma

takeDelimiter 在最后一个段落后产生 null ——即使底层数据以分隔符结束——允许简单的终止检查而无需额外状态。4

注意事项与警告

  • 固定缓冲区是有限的:超出容量会触发可能失败的写入——根据最坏情况格式化输出选择大小。45
  • limited 强制执行硬性上限;原始流的任何剩余部分保持未读取状态(防止过度读取漏洞)。
  • 分隔符流式传输需要非零缓冲区容量;极小的缓冲区可能由于频繁的底层读取而降低性能。39
  • 混合遗留 std.io.fixedBufferStream 和新的 std.Io.* 是安全的,但为了未来的维护,请保持一致性。
  • 通过 buffered().len 计数会排除刷新数据——如果您在管道中间刷新,请使用持久累积器。10

练习

  • 实现一个简单的行计数器,如果任何单行超过256字节则使用 limited 包装器中止。4
  • 构建一个tee,还可以使用哈希写入器适配器中的 Hasher.update 计算所有流式字节的SHA-256哈希。sha2.zig
  • 编写一个基于分隔符+限制的读取器,只从大记录中提取前M个CSV字段,而不读取整行。44
  • 扩展计数示例以在使用 {any} 格式化时跟踪逻辑(格式化后)和原始内容长度。45

警告、替代方案和边缘情况

  • 零容量写入器是合法的,但会立即强制排空——除非有意测试错误路径,否则请避免为了性能而使用。
  • 复制非常大缓冲块的tee循环可能会垄断缓存;对于巨大流,考虑分块以提高局部性。39
  • takeDelimiter 将流结束符视为与分隔符类似;如果您必须区分尾部空段,请跟踪最后一个处理的字节是否为分隔符。31
  • 直接与文件系统API混合(第28章)会引入平台特定的缓冲;在包装操作系统文件描述符时重新验证限制。28
  • 如果未来异步I/O引入暂停点,依赖紧密peek/toss循环的适配器必须确保跨yield的不变性——尽早记录假设。17

Help make this chapter better.

Found a typo, rough edge, or missing explanation? Open an issue or propose a small improvement on GitHub.