乐于分享
好东西不私藏

Zig 源码分析:为什么敢宣称没有隐藏控制流(第17篇)

Zig 源码分析:为什么敢宣称没有隐藏控制流(第17篇)

大家好,我是Zachel,继续我们的Zig深度系列。

Zig官网最显眼的卖点之一,就是这句:

No hidden control flow.If Zig code doesn’t look like it’s jumping away to call a function, then it isn’t.

这句话听起来很硬核,也确实让很多从C++/Rust/D/Go过来的人既震惊又好奇:真的能做到“所见即所得”的控制流吗?defererrdefertryasync这些东西不就是隐藏跳转吗?

今天我们就从源码视角 + 语言设计哲学两个维度,拆开看看Zig为什么敢把“没有隐藏控制流”写进官方宣传里,以及它到底是怎么实现的。

1. 官方定义的“隐藏控制流”到底指什么?

先直接引用ziglang.org/learn/overview 和 why_zig_rust_d_cpp 里的原话(核心段落几乎没变过多年):

var a = b + c.d;foo();bar();

在Zig里,你可以100%确定上面这段代码只会执行:

  • + 运算(一定是内置的算术或指针运算,不会调用函数)
  • foo()
  • bar()

没有其他任何函数会被偷偷调用

而其他语言常见的“隐藏”例子,Zig全部主动避免:

语言
隐藏控制流例子
Zig怎么避免
C++
运算符重载 a + b 调用 operator+
禁止运算符重载
C++
c.d

 是property/getter,偷偷调用函数
没有property,. 永远是字段/方法
C++
对象出作用域自动调用析构函数
没有析构函数,用显式 defer
D
@property

 让字段访问像函数调用
没有这种机制
Rust
?

 运算符在错误时可能跳出 + Drop trait
try

 是显式的,清理用 defer
Go
panic/recover

 像异常一样跳出
没有异常,只有 return error + defer
Java/C#
throw

 异常中断正常流程
没有异常机制

一句话总结:Zig把所有可能的“隐式跳转/隐式调用”都掐死了,只留下显式的关键字和显式的函数调用

2. defer / errdefer 算不算隐藏?

这是问得最多的问题。

很多人第一眼看到:

{    const f = try std.fs.cwd().openFile("data.txt", .{});    defer f.close();    // ... 很多代码}

会想:} 结束时偷偷调用 f.close(),这不就是隐藏控制流吗?

Zig社区和Andrew Kelley本人的回答非常一致

  • defer 的执行位置就在你写它的地方下面,离得很近,可视性极高。
  • 一眼就能看到要清理什么资源。
  • 它不像C++析构函数那样藏在类型定义里、藏在头文件里、可能调用一堆虚函数。
  • 更重要的是:defer 块里的代码就是普通代码,不会再引入新的隐藏跳转。

对比C++:

{std::unique_ptr<File> f = open("data.txt");// f 析构时调用 close(),但 close() 可能被重载、可能抛异常、可能调用其他东西}

你根本不知道 ~unique_ptr() 里到底写了什么。

而Zig的defer:

defer f.close();   // ← 就这一行,close 就是 close,不会变

可见性可预测性是关键指标。

源码里,defer的实现其实很简单(简化版伪码,基于老版本编译器IR):

  • 进入块时,把 defer 语句收集到一个栈/链表里
  • 块退出(return / break / })时,反向执行收集到的defer表达式

没有魔法,就是一个显式的栈结构,和goto cleanup模式本质一样,只是语法糖更友好。

3. try 算隐藏控制流吗?

const data = try parseJson(str);

很多人觉得这行“看起来没写if,却可能跳出函数”。

但Zig官方明确把try归类为显式控制流

  • try 就是 return + 错误传播的简写
  • 函数签名里必须写 !T 才允许使用 try
  • 你一看函数签名就知道“这里可能返回错误”
  • 没有隐式的栈展开、没有异常表、没有额外的运行时开销去找catch

源码层面,try expr 大致降低为:

const tmp = expr;if (tmp) |value| value else |err| return err;

非常直白。

4. async / await / cancel 呢?(最有争议的部分)

Zig的async曾经是最容易被指责“违反no hidden control flow”的地方。

早期(0.10以前)async确实引入了frame、suspend/resume,编译器会偷偷生成状态机,很多人觉得这算隐藏。

但从0.11开始,Zig把async彻底重构(目前最新设计在持续迭代中),核心思路是:

  • async fn 仍然是显式关键字
  • await / cancelawait 是显式调用点
  • 取消时会显式触发 errdefer / defer 清理(类似return error的路径)
  • 没有隐式Future polling、没有隐藏的运行时调度

虽然底层还是状态机,但所有跳转点都是你写出来的awaitcancelawaitsuspend 等。

这和“在+号里偷偷调用函数”或者“对象析构偷偷跳”完全不是一个量级。

小结:Zig为什么“敢”宣称没有隐藏控制流

因为它把“隐藏”的定义卡得很死:

  • 不允许任何语法糖/类型系统机制在你没写函数调用的地方偷偷调用函数
  • 所有非本地跳转必须用显式关键字(if、while、return、try、break、continue、defer、async/await等)
  • 所有资源清理必须就地可见(defer/errdefer)

这套约束让Zig代码的可预测性可grep性可维护性达到了一个新高度,尤其适合系统编程、嵌入式、游戏引擎、性能敏感场景。

欢迎留言讨论:你觉得defer/try/async哪个最接近“隐藏控制流”的边界?

我们下篇见~

(第17篇完)