跳转至

循环与流式 — for 是表达式,错误要分受众,cat 砍掉内存上限

这一章讲什么: 3.2 节给 cat 装上命令行参数,让它能一次拼多个文件—— 顺带把 Zig 的循环全家讲透:for 不只是「遍历」,它是表达式,能返回值。 3.3 节是全书最该记住的一课:写完能跑的版本之后, 停下来审错误、审开销——哪些错误该崩、哪些该报给用户继续跑? 「把整个文件读进内存再打印」这个捷径藏着什么账? 答案是把程序改成流式(reader 到 writer 逐块搬运), 顺手删掉一个看不见的 16 MiB 上限。

1. 这一章讲什么

3.2 节的任务清单很短:用 std.process 拿到命令行参数,用 for 循环挨个读文件。 但作者围绕它把循环讲全了;3.3 节则不调代码先调脑子: 逐条审 try,逐条问「这个错误该由谁看见、看见之后该发生什么」; 然后用操作系统级别的「最少步骤」重审设计,发现读文件的方式本身就是问题, 换成流式读写收尾。

2. 顶层全景:从「能跑」到「敢用」的两级跳

  3.2 版:能跑
    args[1..] 逐个文件 → readFileAlloc → writeAll → flush
      遗留:任何文件打不开 → 整个程序崩
      遗留:文件 > 16 MiB → 报错;读 1 GiB 文件要 1 GiB 内存
  3.3 版:敢用
    错误分受众:打不开的 → 报 stderr 继续;读写出错 → 崩
    读法换流式:reader 逐块 → writer 逐块,无整文件分配、无 16 MiB 上限

  图说:两级跳各自解决一类「能跑但不是好软件」的债。

3. 核心原理

3.1 for 循环:可下标类型上的遍历表达式

基本形。 Zig 的 for 作用在「可下标类型」上:slice、数组、指向数组的指针1。 像 catch 一样,它用捕获组给元素起名2。很多语言里这东西叫 foreach(对每一项做什么)3:

for (args[1..]) |filepath| {
    // filepath 依次是第 1、2、… 个命令行参数
    // args[1..]:跳过 args[0](程序自己的名字)
}

遍历范围。 for (0..4) |i| 直接遍历一段整数——左闭右开,打出 0 1 2 34。

多组数据并遍历(防手滑神器)。 想同时遍历两个等长的数组, 可以先遍历下标再分别取(容易写错);Zig 有更直的选择—— for 可以同时遍历多组数据5:

const produce = [_][]const u8{ "carrots", "peas", "dragonfruit", "avocados" };
const counts  = [_]i32{ 32, 96, 7, 0 };

for (produce, counts) |name, count| {           // 每组一个捕获名
    std.debug.print("{s}: {}\n", .{name, count});
}

再补一个 0.. 就能同时拿到下标:for (produce, counts, 0..) |name, count, i| ——i 从 0 开始,类型 usize6。顺手防住的错:两个数组长度不一致时, 这个写法编译期/运行期都会暴露,而「分别按下标取」会悄悄错位。

for 是表达式:本章主线例子的落点。 作者明说这是他最爱的 for 特性—— for 是表达式,和其他控制流一样7。当表达式用时,循环的值由 break 带出来;而且必须提供一个 else 子句:没有一次 break 触发时, 整个表达式的值就是 else 的值8。合起来就是一个类型安全的「查找」:

fn indexOf(items: []const i32, value: i32) error{NotFound}!usize {
    return for (items, 0..) |item, i| {
        if (item == value) break i;      // 找到:break 带值出去
    } else error.NotFound;               // 没找到:else 兜底
}

主线例子走一遍:在 [45, 23, 64] 里找 64。

  第 1 轮   item = 45, i = 0    45 == 64? 否
  第 2 轮   item = 23, i = 1    23 == 64? 否
  第 3 轮   item = 64, i = 2    64 == 64? 是 → break 2
  表达式值  2                   返回 2
  ── 换找 35 ──
  三轮都否 → else 触发 → 返回 error.NotFound
  图说:数字是程序真实路径;error{NotFound} 把「找不到」编进了返回类型。

continue:提前进下一轮。 不想退出整个循环,只想跳过某项时用 continue——比如只打印有库存的果蔬:if (count == 0) continue;9。

while:稀客,但有独门语法。 while 就是「条件为假就停」,别的语言都熟; 原书用考拉兹(Collatz)序列演示:偶数除 2、奇数乘 3 加 1,直到变 110。 它有个贴心设计:「每轮末尾固定要做的操作」有专用语法—— while (n != 1) : (count += 1) { … } 把递增挂到循环头, 循环体只管业务逻辑11。但作者也坦白:while 在 Zig 里出奇地少, 它有自己的少数特定用场,后面会撞见12。

3.2 审错误:崩溃之前,先问「给谁看」

3.3 节的第一步不是改代码,是过堂。Zig 的显式语法让过堂变得机械: 找到所有 try,就是在找所有「可能失败就崩」的地方—— 「Zig 的显式语法让这件事很直接:找 try 就行」13。

错误的两种受众。 try 一路上抛 = 崩溃。崩溃有时是对的: 有些错误在维护「永远不该破」的约束,那种错误本来就该崩给开发者看。 但有些错误是用户的日常——文件不存在、权限不够——这时崩溃毫无道理。 再往远处想:服务器这类长命程序,任何崩溃都不能接受, 「它宁可把错误记进日志,然后继续跑」14。

把审计落在 cat 的四个 try 上:

try 的位置 受众 3.3 版的策略
argsAlloc(拿命令行参数) 开发者 保持崩溃:没参数 cat 干不了任何事
readFileAlloc(读文件) 用户 改成:报 stderr、跳过这个文件、继续下一个
stdout.print(写输出) 开发者 保持崩溃:输出都没了,cat 没存在的意义
flush(冲刷) 开发者 保持崩溃15

这张表是全章最该带走的东西:「崩不崩」不是技术问题,是产品决定—— 由错误的受众和后果定。

「改进可能是浪费时间」:先算账再动手。 作者在这里插了一节反直觉的 刹车:程序不需要「最优」也不需要「无懈可击」才能成为它最好的样子; 每一行代码都背着一笔人账——开发速度、使用频率与场景、用户友好度、 坏掉的可能与后果、维护成本、运行成本、以及「这个轮子是不是已经有了」16。 对 cat:它是个轮子里的轮子,没几个人用、没关键流程依赖、 用户大概率只有自己——但「把轮子造到核级,正是为了在要紧时刻知道怎么造」17。 所以本章的改进不是为了 cat,是为了练手艺。

3.3 核级改造:流式读写

回到操作系统的账本上。 文件本质是操作系统的抽象,所以用 OS 的词汇 重述「读一个文件」的最少步骤18:

  第 ① 步 打开文件(失败:真 cat 是向 stderr 报一条、继续下一个文件——我们也照办)
  第 ② 步 从文件读数据、写同样的数据到 stdout,直到没有为止
          (中途读写失败:半份文件可能已经出去了,没法恢复——崩溃合理)
  第 ③ 步 关闭文件

顺带一个细节:连「写 stderr 报错」本身都可能失败——但不值得为这个崩溃, 忽略它没问题19。

readFileAlloc 的隐藏账。 拿最少步骤对照现在的代码, readFileAlloc 露出了「不必要的行为」20: 它不是「读一点写一点」,而是先分配一块能装下整个文件的内存、 把文件整个读进去、再一次性给你——把「读」和「写 stdout」之间 凭空插进了一个完整文件的缓冲。 改掉它,这一阶段的潜在「内存不足」错误整个消失, 16 MiB 的上限也一并消失——不再需要给文件大小封顶了21。

reader 与 streaming。 reader 和 writer 同构(第 07 篇的接口模式): std.io.Reader 也是 vtable 泛型接口,能接任何字节的来源。 而当 reader 和 writer 同时在场,就解锁了一个「神秘新能力」:streaming(流式)—— 调一个函数,让 reader 的字节源源不断地进 writer,直到没有为止22。 cat 的新循环体:

for (args[1..]) |filepath| {
    const file = cwd.openFile(filepath, .{}) catch {        // 打开失败:不再崩
        stderr.print("could not open: {s}\n", .{filepath}) catch {};
        stderr.flush() catch {};
        continue;                                          // 报完继续下一个文件
    };
    defer file.close();                                    // 第 ③ 步:关闭

    var file_buffer: [1024]u8 = undefined;                 // 自己带缓冲
    var file_reader = file.reader(&file_buffer);
    const reader = &file_reader.interface;

    _ = try reader.streamRemaining(stdout);                // 第 ② 步:流到 stdout
}
try stdout.flush();

主线例子收尾:跑一遍三个文件。

  命令行:cat meow.txt moo.txt woof.txt
  meow.txt   打开 OK → 1024 字节缓冲,逐块流 → stdout 收到 "meow\n"
  moo.txt    打开失败 → stderr 收到 "could not open: moo.txt" → continue
  woof.txt   打开 OK → 流 → stdout 收到 "woof\n"
  flush      缓冲区清空
  实际输出:
    meow
    could not open: moo.txt
    woof

  图说:输出混在两股流里(stdout 与 stderr),次序取决于终端;
  文件内容、报错文案都是程序真实输出。

对比 3.2 版:同一个文件列表在旧版里会在 moo.txt 处崩溃, 整个 woof.txt 永远打不出来;新版把「尽力而为」还给了 cat——这正是 真 cat 的行为,也是「错误分受众」这张表兑现后的样子。

4. 作者的判断与证据

判断 是书里给的证据,还是作者的推测
「找 try = 找到所有崩溃点」 机制事实(try 语义决定的),可机械执行
读写出错「崩溃合理」 作者判断,给了理由(半份文件已出去,无法恢复)
「改进可能是浪费时间」的七笔账 作者的经验清单,每条合理但无数据
「造核级轮子是为了要紧时刻会造」 学习观断言——本书写作动机的自白
readFileAlloc 的分配是「不必要的行为」 可验证的机制陈述(对照 OS 最少步骤)
streaming 是「简单、可靠、有表现力」的想法 作者评价,例子可跑

5. 边界与局限

  • 1024 字节的块大小是拍脑袋的:流式化只解决了「整文件驻留内存」, 块大小本身没有论证——第 09 节(3.4)正是拿它开刀:两个人要互相冲突的块大小。
  • stdout 的缓冲和文件流之间没有兜底:文件流中途失败时, 已经流出的半份内容和缓冲里未冲的内容,顺序不受控——作者用「没有回头路」 一句带过23,生产级 cat 会更讲究。
  • streamRemaining 的返回值被 _ = 丢弃:流了多少字节没人记, 书里没有解释为什么返回值存在(对称性/未来扩展)。
  • 「七笔账」清单缺了第八笔:安全面。 面向不可信输入的程序, 「坏掉的后果」一栏默认要加重——这份清单更适合工具型程序。

6. 可带走的

  1. for 遍历「可下标类型」;捕获组命名元素;args[1..] 跳过程序名;
  2. for 能同时遍历多组数据,长度不一致会暴露;加 0.. 拿带下标;
  3. for 是表达式:break 带值、else 兜底,「查找」写成十行类型安全的代码;
  4. continue 跳本轮;while 稀有,但「每轮末尾操作」有 : (…) 专用语法;
  5. 审错误的第一动作:搜 try——每个 try 都是一个「放任崩溃」的决定;
  6. 错误分受众:开发者错误崩,用户错误报完继续,服务器写日志;
  7. 「改进前先算七笔账」,「轮子要不要造到核级」取决于学习目标;
  8. 用「操作系统最少步骤」当照妖镜:readFileAlloc 的整文件缓冲现行;
  9. reader + writer = streaming:一个调用逐块搬运,无整文件内存、无大小上限;
  10. 打开失败 catch { …; continue; } 的写法,就是「尽力而为」四个字的形状。

7. 原文地图

主题 原书章 原文位置
命令行参数与双横线 3.2 Giving our cat more toys text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:47(搜「argsAlloc」) · text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:82(搜「two dashes」)
for 作用域与捕获组;foreach 提示 3.2 Giving our cat more toys text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:88(搜「indexable types」) · text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:92(搜「foreach」)
同时遍历多组数据;带下标 3.2 Giving our cat more toys text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:119(搜「multiple sets of data」) · text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:136(搜「alongside a range」)
for 是表达式:break 与 else 3.2 Giving our cat more toys text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:146(搜「My favorite feature」) · text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:148(搜「else clause」)
indexOf 主线例子 3.2 Giving our cat more toys text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:155(搜「NotFound」)
continue 3.2 Giving our cat more toys text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:174(搜「count == 0」)
Collatz 与 while 的尾部语法 3.2 Giving our cat more toys text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:187(搜「Collatz」) · text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:214(搜「built-in syntax」) · text/18-ch03-03-3-3-questioning-our-cat.txt:18(搜「surprisingly rare」)
错误分受众;服务器记日志 3.3 Questioning our cat text/18-ch03-03-3-3-questioning-our-cat.txt:40(搜「log errors to a file」) · text/18-ch03-03-3-3-questioning-our-cat.txt:45(搜「look for try」)
「改进可能是浪费时间」与七笔账 3.3 Questioning our cat text/18-ch03-03-3-3-questioning-our-cat.txt:88(搜「Improvement can be a waste of time」) · text/18-ch03-03-3-3-questioning-our-cat.txt:96(搜「Development speed」)
「核级轮子」 3.3 Questioning our cat text/18-ch03-03-3-3-questioning-our-cat.txt:112(搜「If anything is a wheel」)
OS 最少三步 3.3 Questioning our cat text/18-ch03-03-3-3-questioning-our-cat.txt:124(搜「Open the file」) · text/18-ch03-03-3-3-questioning-our-cat.txt:135(搜「Close the file」)
readFileAlloc 的隐藏账 3.3 Questioning our cat text/18-ch03-03-3-3-questioning-our-cat.txt:140(搜「unnecessary behavior」) · text/18-ch03-03-3-3-questioning-our-cat.txt:143(搜「out of memory」)
streaming 与 streamRemaining 3.3 Questioning our cat text/18-ch03-03-3-3-questioning-our-cat.txt:152(搜「streaming」) · text/18-ch03-03-3-3-questioning-our-cat.txt:195(搜「streamRemaining」)
「could not open」实测输出 3.4 Buffering and building with Buff and Bill text/19-ch03-04-3-4-buffering-and-building-with-buff-and-bill.txt:4(搜「meow.txt moo.txt woof.txt」)

  1. 出处:「3.2 Giving our cat more toys」第 87–90 段(3.2.1,text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:88,搜「indexable types」)。原文:「Zig's for loops operate on indexable types like slices, arrays, and pointers to arrays. Similarly to catch, they use capture group syntax to assign a name to the items being iterated over.」 ↩

  2. 出处:同上,捕获组句同段(text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:89,搜「capture group syntax」)。 ↩

  3. 出处:「3.2 Giving our cat more toys」第 92 段(TIP,text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:92,搜「foreach」)。原文:「In many programming languages, what Zig calls a for loop is often called a foreach loop.」 ↩

  4. 出处:「3.2 Giving our cat more toys」第 96–104 段(text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:96,搜「iterate through a range」)。 ↩

  5. 出处:「3.2 Giving our cat more toys」第 119–122 段(text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:119,搜「multiple sets of data」)。原文:「For loops can actually loop over multiple sets of data at once. This is not only terser but can also prevent programmer error.」 ↩

  6. 出处:「3.2 Giving our cat more toys」第 135–144 段(listing 3.10,text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:136,搜「alongside a range」)。 ↩

  7. 出处:「3.2 Giving our cat more toys」第 146–147 段(text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:146,搜「My favorite feature」)。原文:「My favorite feature of Zig's for loops is that they are expressions, just like other control flow.」 ↩

  8. 出处:「3.2 Giving our cat more toys」第 147–151 段(text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:148,搜「else clause」)。原文:「the value of the expression will be returned through a break statement. You must provide an else clause, which will be the result of the expression in the case that a break is never triggered.」 ↩

  9. 出处:「3.2 Giving our cat more toys」第 163–179 段(listing 3.12,text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:167,搜「continue on to the next iteration」)。 ↩

  10. 出处:「3.2 Giving our cat more toys」第 183–191 段(3.2.2,text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:187,搜「Collatz」)。原文:「We start with a number. If it's even, divide it by two. If it's odd, multiply it by 3 and add one. Eventually it reaches one.」 ↩

  11. 出处:「3.2 Giving our cat more toys」第 213–216 段与「3.3 Questioning our cat」第 1–15 段(listing 3.14,text/17-ch03-02-3-2-giving-our-cat-more-toys.txt:214,搜「built-in syntax」)。原文:「while loops actually have built-in syntax for that!」 ↩

  12. 出处:「3.3 Questioning our cat」第 17–19 段(text/18-ch03-03-3-3-questioning-our-cat.txt:18,搜「surprisingly rare」)。原文:「While loops are surprisingly rare in Zig, but they have some tailored use cases we will encounter later on.」 ↩

  13. 出处:「3.3 Questioning our cat」第 31–45 段(3.3.1,text/18-ch03-03-3-3-questioning-our-cat.txt:45,搜「look for try」)。原文:「Zig's explicit syntax makes this straightforward: all you have to do is look for try.」 ↩

  14. 出处:「3.3 Questioning our cat」第 33–41 段(text/18-ch03-03-3-3-questioning-our-cat.txt:40,搜「log errors to a file」)。原文:「For long-running applications like servers, any crashes whatsoever might be undesirable. A server might instead wish to log errors to a file and continue running.」 ↩

  15. 出处:「3.3 Questioning our cat」第 47–82 段(listing 3.15 的 #1–#4 注释,text/18-ch03-03-3-3-questioning-our-cat.txt:79,搜「crashing here is fine」;:80,搜「don't want to crash」)。四条注释逐条标出哪个 try 保持崩溃、哪个改为报错继续。 ↩

  16. 出处:「3.3 Questioning our cat」第 87–111 段(3.3.2,text/18-ch03-03-3-3-questioning-our-cat.txt:88,搜「Improvement can be a waste of time」;:96,搜「Development speed」)。七笔账逐条照原文:Development speed / Frequency and kind of usage / User friendliness / Likelihood and impact of breakage / Maintenance / Operating cost / Reinventing wheels。 ↩

  17. 出处:「3.3 Questioning our cat」第 112–114 段(text/18-ch03-03-3-3-questioning-our-cat.txt:112,搜「If anything is a wheel」)。原文:「If anything is a wheel, it's cat.」「However, reinventing wheels and making them nuclear grade is the best way to know how to do it when it counts.」 ↩

  18. 出处:「3.3 Questioning our cat」第 116–139 段(3.3.3,text/18-ch03-03-3-3-questioning-our-cat.txt:124,搜「Open the file」;:135,搜「Close the file」)。三步及各自的失败策略均照原文编号列表。 ↩

  19. 出处:「3.3 Questioning our cat」第 137–138 段(text/18-ch03-03-3-3-questioning-our-cat.txt:137,搜「not worth crashing」)。原文:「writing error messages to stderr might fail, too! It's not worth crashing over, though.」 ↩

  20. 出处:「3.3 Questioning our cat」第 139–145 段(text/18-ch03-03-3-3-questioning-our-cat.txt:140,搜「unnecessary behavior」)。原文:「our code using readFileAlloc in listing 3.15 involves unnecessary behavior. Instead of reading from a file and immediately writing to stdout, it allocates a buffer that is big enough to store the entire file, and then reads data into it before returning it to us.」 ↩

  21. 出处:「3.3 Questioning our cat」第 143–145 段(text/18-ch03-03-3-3-questioning-our-cat.txt:143,搜「out of memory」)。原文:「By removing the allocation, we would prevent any potential out of memory errors at this stage in the program and remove any requirement to cap the file size.」 ↩

  22. 出处:「3.3 Questioning our cat」第 147–158 段(3.3.4,text/18-ch03-03-3-3-questioning-our-cat.txt:152,搜「streaming」)。原文:「once both readers and writers are in the mix, we are granted access to a mystical new power: streaming.」「we can call a single function to take the bytes from the reader and shove them into the writer until there are none remaining.」 ↩

  23. 出处:「3.3 Questioning our cat」第 205 段(listing 3.16 的 #6 注释,text/18-ch03-03-3-3-questioning-our-cat.txt:205,搜「no going back」)。 ↩