Zig vs Rust in 2026
大约三年前,在编程智能体(coding agents)出现之前,我曾用 Zig 和 unsafe Rust 编写过一个字节码虚拟机和垃圾回收器。当时我觉得,从人类工效学(human ergonomics)的角度来看,编写 unsafe 代码时 Zig 更胜一筹。
遗憾的是,如今我亲手编写的代码越来越少,这意味着我使用 Zig 的理由也越来越少。Zig 的特性所带来的 1.5 到 5 倍的人类开发效率提升,在编程智能体为 Rust 带来的 100 倍效率提升面前显得微不足道。Zig 的许多伟大特性都是为了人类工效学而设计的,但这对智能体来说并不重要。
我想谈谈我最喜欢的那些 Zig 特性,以及为什么它们现在显得没那么重要了。
分配器接口(Allocator interface)
这是我最喜欢的 Zig 特性。当你使用专门的分配器来优化代码路径(如 arena、栈回退等)时,会有一种“大脑升级”的感觉。
// A real example:
// Reading a line of user input requires a heap allocator because
// in theory the input length is unbounded. In practice though,
// the input is almost always a short search query or path that
// fits in well under 1kb.
//
// stackFallback puts a fixed-size buffer on the stack and only
// hits the heap when the input overflows it. The common case
// costs zero heap allocations. The rare long input still works
// because the allocator silently upgrades to the heap.
var stack_fallback = std.heap.stackFallback(256, heap_allocator);
const alloc = stack_fallback.get();
const line = try reader.readUntilDelimiterAlloc(alloc, '\n', 4096);
defer alloc.free(line);
(大意:Zig 允许轻松通过 stackFallback 在栈上分配内存,仅在溢出时才退回堆分配。这是 Zig 高性能内存管理的精髓。)
过去,Rust 的问题在于没有等价的分配器接口。如果你想要一个使用自定义分配器的 Vec<T>,你只能复制粘贴标准库版本并进行修改(Bumpalo 库就是这么做的,它本质上是标准库集合的副本)。
现在,Rust 的 nightly 版本中已经有了 Allocator trait,而且表现不错。由于它是 trait,所以是静态分派,而 Zig 是基于 vtable 的。虽然社区没有像 Zig 那样形成通用的“基于分配器进行数据结构设计”的惯例,但 AI 改变了游戏规则——它让复制粘贴代码并进行修改变得极其简单。对于我的使用场景来说,这已经足够好了。
任意位宽整数 + 紧凑结构体(Packed structs)
这是我钟爱的另一个 Zig 特性。它让 DOD(面向数据)风格的 CPU 缓存优化、标签指针(tagged pointers)、NaN 装箱等变得非常简单,甚至连创建位标志(bitflags)都变得轻而易举。
pub const TaggedClass = enum(u3) {
ns_atom = 0,
ns_string = 1,
ns_number = 2,
ns_date = 3,
};
pub const ObjcTaggedPointer = packed struct {
is_tagged: bool = true,
class: TaggedClass,
payload: u60,
pub fn ns_number(n: u60) ObjcTaggedPointer {
return .{ .class = .ns_number, .payload = n };
}
pub fn from_raw(raw: u64) ObjcTaggedPointer {
return @bitCast(raw);
}
pub fn raw(self: ObjcTaggedPointer) u64 {
return @bitCast(self);
}
pub fn is_ns_number(self: ObjcTaggedPointer) bool {
return self.is_tagged and self.class == .ns_number;
}
};
在 Rust 语言中,类似 OR 的操作是在构造时进行的,并且在每次访问时都会将其屏蔽掉。该槽位只是一个 u64 常量,并非真正的类型:
pub struct ObjcTaggedPointer(u64);
impl ObjcTaggedPointer {
const TAG_MASK: u64 = 0b1;
const CLASS_MASK: u64 = 0b1110;
const CLASS_SHIFT: u64 = 1;
const PAYLOAD_SHIFT: u64 = 4;
const CLASS_NS_ATOM: u64 = 0;
const CLASS_NS_STRING: u64 = 1;
const CLASS_NS_NUMBER: u64 = 2;
const CLASS_NS_DATE: u64 = 3;
pub fn ns_number(n: u64) -> Self {
Self(
(n << Self::PAYLOAD_SHIFT)
| (Self::CLASS_NS_NUMBER << Self::CLASS_SHIFT)
| Self::TAG_MASK,
)
}
pub fn is_ns_number(self) -> bool {
self.0 & Self::TAG_MASK != 0
&& (self.0 & Self::CLASS_MASK) >> Self::CLASS_SHIFT == Self::CLASS_NS_NUMBER
}
}
你可以看出,原生的 Rust 实现方式缺乏工效学。你不得不依赖像 bitfield 或 bitflags 这样的库,它们往往依赖过程宏(proc macro)的魔术,我觉得不如 Zig 的紧凑结构体直观。
然而,在有了编程智能体之后,我根本不在乎手动编写这些代码有多麻烦。
Comptime
这是 Zig 最炫酷的特性。除了可能存在的某些生僻的依赖类型语言外,没有其他编程语言拥有像 Zig 这样优秀的编译期求值能力。
我原以为我会非常想念它,但实际上并没有。对我来说,95% 的 comptime 使用场景是创建类似泛型的数据结构。Rust 在这方面的类型系统设计我认为更好(详见下文)。
至于剩下的 5% 情况,没有 comptime 的确很痛苦,唯一的替代方案是代码生成(codegen)。我现在正在做一个游戏,通过工具生成的硬编码碰撞盒几何数据需要植入到数据结构中。没有 comptime,我不得不让 Claude 写一个生成 Rust 文件的脚本。不过,我发现自己并没有那么频繁地需要编译期求值。
Rust 的类型系统
我想我宁愿牺牲 comptime,去换取 Rust 设计得更好的类型系统,特别是在边界多态(traits/typeclasses)方面。在 Zig 中尝试实现等价功能简直是噩梦。
此外,我认为 Rust 的类型系统允许你强制执行更多的不变量,并防止编程智能体犯常见错误。在我的游戏中,我使用了 euclid 库。它通过为每个坐标空间创建专门的类型(例如 Point<Screen> 或 Point<World>),有效地防止了坐标系混淆(图形编程中非常常见的问题)。
这让智能体无法犯下将世界坐标与屏幕坐标混淆的愚蠢错误。
不必处理内存问题
编程智能体让代码产出量提升了 100 倍,这意味着你需要审查的内存安全风险也增加了 100 倍。在没有形式化验证的情况下,为了发现 bug 而需要遍历的代码搜索空间太大了。
面对如今爆发式增长的代码量,Rust 的吸引力更强了。Rust 的代价一直在于它会阻碍开发效率,特别是如果你不熟悉借用检查器(borrow checker),但在编程智能体时代,这根本不再是问题。
而且,如果你在 Rust 中使用 unsafe,还有像 miri 这样的工具。你可以让编程智能体运行代码并通过 miri 检测,以确保它不会导致未定义行为(UB)或违反 Rust 的别名规则。
总结
我依然怀念编写 Zig 的日子,并认为它是一门优秀的语言。但我现在更喜欢 Rust,而且编程智能体与 Rust 的配合更加默契。
加入我们
Zig 中文社区是一个开放的组织,我们致力于推广 Zig 在中文群体中的使用,有多种方式可以参与进来:
- 供稿,分享自己使用 Zig 的心得
- 改进 ZigCC 组织下的开源项目
- 加入微信群、Telegram 群组
大约三年前,在编程智能体(coding agents)出现之前,我曾用 Zig 和 unsafe Rust 编写过一个字节码虚拟机和垃圾回收器。当时我觉得,从人类工效学(human ergonomics)的角度来看,编写 unsafe 代码时 Zig 更胜一筹。
遗憾的是,如今我亲手编写的代码越来越少,这意味着我使用 Zig 的理由也越来越少。Zig 的特性所带来的 1.5 到 5 倍的人类开发效率提升,在编程智能体为 Rust 带来的 100 倍效率提升面前显得微不足道。Zig 的许多伟大特性都是为了人类工效学而设计的,但这对智能体来说并不重要。
我想谈谈我最喜欢的那些 Zig 特性,以及为什么它们现在显得没那么重要了。
分配器接口(Allocator interface)
这是我最喜欢的 Zig 特性。当你使用专门的分配器来优化代码路径(如 arena、栈回退等)时,会有一种“大脑升级”的感觉。
(大意:Zig 允许轻松通过
stackFallback在栈上分配内存,仅在溢出时才退回堆分配。这是 Zig 高性能内存管理的精髓。)过去,Rust 的问题在于没有等价的分配器接口。如果你想要一个使用自定义分配器的
Vec<T>,你只能复制粘贴标准库版本并进行修改(Bumpalo 库就是这么做的,它本质上是标准库集合的副本)。现在,Rust 的 nightly 版本中已经有了
Allocatortrait,而且表现不错。由于它是 trait,所以是静态分派,而 Zig 是基于 vtable 的。虽然社区没有像 Zig 那样形成通用的“基于分配器进行数据结构设计”的惯例,但 AI 改变了游戏规则——它让复制粘贴代码并进行修改变得极其简单。对于我的使用场景来说,这已经足够好了。任意位宽整数 + 紧凑结构体(Packed structs)
这是我钟爱的另一个 Zig 特性。它让 DOD(面向数据)风格的 CPU 缓存优化、标签指针(tagged pointers)、NaN 装箱等变得非常简单,甚至连创建位标志(bitflags)都变得轻而易举。
在 Rust 语言中,类似 OR 的操作是在构造时进行的,并且在每次访问时都会将其屏蔽掉。该槽位只是一个 u64 常量,并非真正的类型:
你可以看出,原生的 Rust 实现方式缺乏工效学。你不得不依赖像
bitfield或bitflags这样的库,它们往往依赖过程宏(proc macro)的魔术,我觉得不如 Zig 的紧凑结构体直观。然而,在有了编程智能体之后,我根本不在乎手动编写这些代码有多麻烦。
Comptime
这是 Zig 最炫酷的特性。除了可能存在的某些生僻的依赖类型语言外,没有其他编程语言拥有像 Zig 这样优秀的编译期求值能力。
我原以为我会非常想念它,但实际上并没有。对我来说,95% 的 comptime 使用场景是创建类似泛型的数据结构。Rust 在这方面的类型系统设计我认为更好(详见下文)。
至于剩下的 5% 情况,没有 comptime 的确很痛苦,唯一的替代方案是代码生成(codegen)。我现在正在做一个游戏,通过工具生成的硬编码碰撞盒几何数据需要植入到数据结构中。没有 comptime,我不得不让 Claude 写一个生成 Rust 文件的脚本。不过,我发现自己并没有那么频繁地需要编译期求值。
Rust 的类型系统
我想我宁愿牺牲 comptime,去换取 Rust 设计得更好的类型系统,特别是在边界多态(traits/typeclasses)方面。在 Zig 中尝试实现等价功能简直是噩梦。
此外,我认为 Rust 的类型系统允许你强制执行更多的不变量,并防止编程智能体犯常见错误。在我的游戏中,我使用了
euclid库。它通过为每个坐标空间创建专门的类型(例如Point<Screen>或Point<World>),有效地防止了坐标系混淆(图形编程中非常常见的问题)。这让智能体无法犯下将世界坐标与屏幕坐标混淆的愚蠢错误。
不必处理内存问题
编程智能体让代码产出量提升了 100 倍,这意味着你需要审查的内存安全风险也增加了 100 倍。在没有形式化验证的情况下,为了发现 bug 而需要遍历的代码搜索空间太大了。
面对如今爆发式增长的代码量,Rust 的吸引力更强了。Rust 的代价一直在于它会阻碍开发效率,特别是如果你不熟悉借用检查器(borrow checker),但在编程智能体时代,这根本不再是问题。
而且,如果你在 Rust 中使用 unsafe,还有像
miri这样的工具。你可以让编程智能体运行代码并通过miri检测,以确保它不会导致未定义行为(UB)或违反 Rust 的别名规则。总结
我依然怀念编写 Zig 的日子,并认为它是一门优秀的语言。但我现在更喜欢 Rust,而且编程智能体与 Rust 的配合更加默契。
加入我们
Zig 中文社区是一个开放的组织,我们致力于推广 Zig 在中文群体中的使用,有多种方式可以参与进来: