乐于分享
好东西不私藏

Zig for 循环源码为什么比 iterator 更变态 (第27篇)

Zig for 循环源码为什么比 iterator 更变态 (第27篇)

大家好,我是 Zachel ,继续我们的Zig源码学习。今天聊一个看似“普通”的语法——for循环,却藏着让编译器“变态”到极致的黑科技。

很多语言里,for循环背后其实是迭代器(iterator)在偷偷干活:Rust有Iterator trait、Python有__iter____next__,你写个自定义容器就得老老实实实现next()。但Zig不一样——它压根不给你iterator这个轮子,直接把for做成语言级原语,让编译器在源码层面玩出花来。为什么说它“比iterator更变态”?因为你写一行for,编译器干的活比你手写一个高性能iterator还多、还狠、还零成本。

先看表面:Zig的for到底有多灵活?

// 基础版
const items = [_]u32{ 1, 2, 3, 4 };
for (items) |item| {
    std.debug.print("{d} ", .{item});
}

// 带索引(最常用)
for (items, 0..) |item, i| {
    std.debug.print("items[{d}] = {d}\n", .{ i, item });
}

// 多序列同时迭代(0.11+的杀手级特性)
const a = [_]u32{10, 20, 30};
const b = [_]u32{1, 2, 3};
const c = [_]u32{100, 200, 300};
for (a, b, c) |x, y, z| {
    std.debug.print("{d} + {d} + {d} = {d}\n", .{ x, y, z, x + y + z });
}

看起来简单?但编译器在这一行里已经偷偷做了长度一致性检查捕获方式转换指针/值语义,而且全是零运行时开销(comptime已知长度时连运行时检查都省了)。

更变态的:编译器源码里的魔法

Zig编译器(自托管后全Zig写)在src/Sema.zig里处理for循环。核心流程是:

  1. Parser → AstGen 把for语法转成ZIR(Zig Intermediate Representation)指令。
  2. Sema.analyzeBodyInner 这个大switch里,遇到for相关的ZIR tag后,进入专门的语义分析逻辑。
  3. 关键黑科技在这里:多序列长度匹配

源码里有一段经典的长度检查(简化版,从Sema.zig实际代码提取):

// 大致逻辑(实际代码更复杂,涉及InternPool和comptime求值)
if (!sema.valuesEqual(len_a, len_b, .usize)) {
    // comptime已知长度不同 → 直接编译错误
    // runtime长度不同 → 生成运行时检查(safety模式)
    return sema.errMsg(src, "non-matching for loop lengths", .{});
}

这意味着:

  • 如果所有序列的长度在comptime已知且一致 → 编译器直接生成单个循环,连一次长度检查都不剩。
  • 如果长度运行时才知道 → 只生成一次边界检查,后面所有迭代复用这个结果。
  • 支持任意数量的序列(理论上你for 10个数组也没问题)。

这比手写iterator狠多了。你要是自己写个struct Iterator { fn next(self: *@This()) ?T },每次next()都要函数调用、状态机、可能还有虚表开销。Zig的for直接在代码生成阶段(AIR → LLVM Backend)展开成最原始的指针递增或索引循环,和手写C for循环几乎一模一样——零抽象成本

捕获语法背后的指针魔法

for (items) |*item| {  // 可变引用
    item.* += 1;
}

这个|*item|不是语法糖,是编译器在Sema阶段就把捕获变量强制转成指针,并在AIR里生成ptr指令。整个循环体里对item的操作直接变成内存写,没有临时拷贝

如果你用iterator实现同样的功能,得自己维护一个可变引用状态,还得小心生命周期——Zig编译器全给你包了。

为什么不搞iterator?因为“变态”就够了

Zig的设计哲学是“no hidden control flow”。iterator的next()调用是隐藏的控制流,编译器很难全量优化。Zig直接把for做成语言内置的控制流原语,让编译器在源码层面就能看到全部信息:

  • 循环次数(comptime可知时)
  • 所有序列的内存布局
  • 安全检查的位置

结果就是:同样的代码,Zig的for往往比Rust的iterator for更快(尤其在data-oriented design里,多序列for配合SoA结构体,缓存命中率起飞)。

社区里有人吐槽:“Zig为啥不加iterator trait?” 答案很简单——不需要for已经把iterator该干的活干得更狠、更彻底。你想自定义迭代?用while + next()也行,但99%场景下,标准库的for就够了,而且性能更好。

小结:源码里的“变态”才是Zig的真香

for循环表面是语法,底层是Zig编译器在Sema.zig里用ZIR→AIR的管道,把你一行代码翻译成近乎汇编级的极致循环。这就是为什么Zig官方文档和社区一直推“多用for,少自己造iterator轮子”——因为编译器比你更懂怎么循环。

想自己验证?

  1. zig ast-check -t yourfile.zig看ZIR
  2. --verbose-air看编译器到底生成了什么
  3. 对比自己手写iterator的汇编输出(-O ReleaseFast下差距明显)

喜欢就点在看 + 转发给你的Zig小伙伴~
我们下期见!

(所有示例均基于Zig 0.14+,源码参考官方master分支Sema.zig。如有更新,以最新源码为准)