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过来的人既震惊又好奇:真的能做到“所见即所得”的控制流吗?defer、errdefer、try、async这些东西不就是隐藏跳转吗?
今天我们就从源码视角 + 语言设计哲学两个维度,拆开看看Zig为什么敢把“没有隐藏控制流”写进官方宣传里,以及它到底是怎么实现的。
1. 官方定义的“隐藏控制流”到底指什么?
先直接引用ziglang.org/learn/overview 和 why_zig_rust_d_cpp 里的原话(核心段落几乎没变过多年):
var a = b + c.d;foo();bar();
在Zig里,你可以100%确定上面这段代码只会执行:
-
+运算(一定是内置的算术或指针运算,不会调用函数) -
foo() -
bar()
没有其他任何函数会被偷偷调用。
而其他语言常见的“隐藏”例子,Zig全部主动避免:
|
|
|
|
|---|---|---|
|
|
a + b 调用 operator+ |
|
|
|
c.d
|
. 永远是字段/方法 |
|
|
|
defer |
|
|
@property
|
|
|
|
?
|
try
defer |
|
|
panic/recover
|
return error + defer |
|
|
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、没有隐藏的运行时调度
虽然底层还是状态机,但所有跳转点都是你写出来的:await、cancelawait、suspend 等。
这和“在+号里偷偷调用函数”或者“对象析构偷偷跳”完全不是一个量级。
小结:Zig为什么“敢”宣称没有隐藏控制流
因为它把“隐藏”的定义卡得很死:
-
不允许任何语法糖/类型系统机制在你没写函数调用的地方偷偷调用函数 -
所有非本地跳转必须用显式关键字(if、while、return、try、break、continue、defer、async/await等) -
所有资源清理必须就地可见(defer/errdefer)
这套约束让Zig代码的可预测性、可grep性、可维护性达到了一个新高度,尤其适合系统编程、嵌入式、游戏引擎、性能敏感场景。
欢迎留言讨论:你觉得defer/try/async哪个最接近“隐藏控制流”的边界?
我们下篇见~
(第17篇完)
夜雨聆风