做了七年 Rust 的人把同一个查询语言用 Zig 重写一遍,差距记在五个地方

一位做了七年 Rust 的开发者,把自己已经写完的 JSONPath 查询语言实现用 Zig 重写一遍,逐维度记差距。同一套规范 RFC 9535,同一套功能,写两遍,每个摩擦点都有对照组。结论有取舍,Zig 快、直接,有当 C 继任者的潜力,编辑器、函数式写法、分配器纪律、库生态四处要另付代价。这是单一项目的个人体验,样本偏差明显。当成本清单看。

从 Rust 转到 Zig,第一课为什么是没有 IDE

最意外的一点,Zig 几乎没有 IDE 支持。相比 JetBrains 系的 RustRover,只剩语法高亮和基础补全,他被迫退回命令行。build.zig 提供入口,全部测试用 zig build test,单个用例加 -Dfilter,合规套件是 zig build compliance,两者合并是 zig build check。他的评价是接受这些条款之后反而清爽。迁移前先想清楚。IDE 缺失能用命令行补,静态分析兜不住内存错误。

文件结构扁平,为什么比层级好读

同一项目的 Rust 侧有 parser 和 query 两个子目录,底下再分十几个文件。Zig 侧五个平铺文件就完了。作者认为 Zig 不鼓励建目录,嵌套文件带来 import 摩擦,相关代码收进单一大文件、按节导航更可读。他给了边界。不相信这能推广到大型项目,某个点上仍需要真正的层级,门槛只是比他预期的高。忍不住想建文件夹,先当项目还不够大的信号。

为什么函数式组合子写不下去

Rust 版重度使用函数式手段,带 map 与 flat_map 的 monadic 数据结构、纯不可变变换、组合子代替循环、闭包做局部抽象,Zig 版逐个退回循环。查询选择器那一处,Rust 是 map 接 reduce,Zig 是先 fork 一份游标深拷贝再逐层处理。原因不玄。每个函数都要跟 allocator 打交道,纯函数式意味着不断构造新结构,内存开销和手工记账总要选一头付。只有 sum 类型两边接近。没有编译期 trait,Zig 只能用 @typeInfo@hasDecl(T, "query") 在运行期判断方法存在与否,属于鸭子类型。作者明说这是主观感受,Zig 版代码更难读。

allocator 纪律比所有权多付多少代价

Zig 里 allocator 无处不在,几乎每个函数收一个,每个结构体存一个。麻烦在调用栈一长,init 与 deinit 的配对就走样。分配、处理失败路径、决定谁释放,每一步都要人做。四个 bug 都出在这。

忘记 deinit。迭代器 init 之后没有释放,MemoryLeakDetected 指出泄漏来自一次 dupe 调用,修法是 init 之后立刻 defer deinit。错误路径漏 deinit。第二次 append 失败导致迭代器泄漏,FailingAllocator{ .fail_index = 1 } 强制这次分配失败把它暴露,修法换成 errdefer。

测试设施抓得住内存 bug,为什么还不够

后两个更能说明边界。一个是跨函数所有权。三层调用里 cache 持有 result 的浅拷贝指针,第二层却又 defer 了 result 的释放,第二次查询跑到清理阶段,std.testing.allocator 报 DoubleFree 并指出两个释放点。修法是理清归属,cache 活得更久,所有权归第三层,交出之后不能再 defer 释放。另一个是孤儿分配。先 dupe 一份路径,紧接着数组扩容失败,缺一条 errdefer,iter.deinit() 正常跑完也看不到这条泄漏。

作者的归因值得记。Zig 测试更麻烦的根子是手动内存管理,不是设施本身。TestAllocator 抓不了一切,失败路径的用例仍要亲手写。Rust 侧则是另一回事,Drop 在作用域结束自动执行,包括 ? 提前返回,跨函数持有浅拷贝根本编译不过,push 是移动语义。作者补了句公平话,Rust 也能泄漏,Rc 循环和显式 Box::leak 都做得到,永不泄漏要刻意触发。

已经是稳定版了生态为什么还是不够用

Zig 官方当前稳定版是 0.16.0,2026 年 4 月发布,1.0 仍是路线图目标,没有时间表。版本号以 0 开头,标准库 API 逐版本变化就是常态,0.16.0 的发布说明里满是破坏性变更和迁移条目。生态缺口很具体。实现 filter 函数时,作者需要正则里的 Unicode 属性转义,Zig 生态的正则引擎 mvzr 不支持 \p{...},只能绕开。强依赖三方库和 Unicode 级正则的项目,迁过去要接受自己造轮子。

这份记录的边界在哪

作者自述 Zig 经验从这个项目才开始,部分观察对日常用户会显得浅,一些决定被 Rust 习惯塑造。载体是解析与遍历为主的实现,不等于嵌入式、游戏、内核这类 Zig 常见场景。全文没有性能数据,快只是主观形容。可复用的有两个。把已在旧语言完成的项目重写一遍当对比,摩擦点都有现成对照组。再从第一天起按 defer 与 errdefer 的纪律写,先用 TestAllocator 和 FailingAllocator 把失败路径测一遍,然后才动复杂逻辑。

要函数式组合子、不可变数据和成熟三方库的人,迁移过去要重写代码风格。要找 C 继任者、能自己造轮子的人,值得投入。