从Rust转向Zig的感受

Lobsters Hottest 新闻

摘要

一位经验丰富的Rust开发者分享了通过重新实现JSONPath工具来学习Zig的旅程,重点介绍了IDE支持和代码组织方面的差异。

<p><a href="https://lobste.rs/s/oaybfe/what_zig_felt_like_coming_from_rust">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/08/20 10:41

# 从Rust转向Zig的体验 来源:https://besok.github.io/posts/what-zig-felt-like-coming-from-rust/ ## 引言 过去7年我一直是Rust开发者,主要参与开源项目。自认为已对语言及其生态系统建立了扎实的理解。我偏爱Rust的函数式风格——简洁的函数、富有表现力的类型等。但我始终对其他语言充满好奇,而Zig作为C的潜在继任者,长期列入我的关注列表:更底层、更轻量,并逐渐在严肃语言圈站稳脚跟。早年我也曾使用C开发,因此这种对比必然引人深思。 需要提前说明:我对Zig的接触始于本项目。其中一些观察在Zig日常开发者眼中可能显得幼稚且显而易见,某些决策也未必最优——更多是受Rust习惯影响,而非深谙Zig惯用法。这没关系,万事皆有开端。无论如何,我正凭借多年积累的跨语言直觉(无论好坏)进行探索。 为确保公平比较,我选择用Zig重新实现已在Rust中完成的项目——既非玩具级应用,也非庞然大物,最好是社区能实际使用的工具。最终选定JSONPath:一种符合RFC 9535规范的JSON查询语言。Rust版本已存在(jsonpath-rust),目标是将其移植到Zig:zig-jsonpath。 ## IDE支持 首先令我意外的是——老实说,谁能想到这会成为难忘的部分——IDE支持几乎不存在。我习惯为Rust使用RustRover,其他语言则搭配各种JetBrains IDE,相比之下Zig仅提供语法高亮和基础自动补全。这虽不意外,却迫使我回归本质:学习通过命令行驾驭语言。 起初的劣势逐渐转变为有趣的体验。我发现自己早已遗忘纯命令行工具的简洁高效。第一课来自`build.zig`,它以惊人简洁处理构建流程。最终形成如下配置: ``` zig build test # 运行所有测试 zig build test -Dfilter="filter match function basic" # 运行单个测试 zig build test -Ddebug-query=true # 带调试信息运行所有测试 zig build compliance # 合规性测试套件 zig build check # 单元测试+合规性测试 ``` 一旦接受这种模式,工作体验异常清爽。感谢Zig间接促成了更大转变:从依赖完整IDE转向helix + alacritty + zellij(https://github.com/besok/dotfiles)的极简环境。 ## 扁平化结构 在Rust及多数语言中,我常花费大量时间在文件大小与目录深度间权衡。理论上可自由拆分文件、无限延伸目录层级。Zig虽同样允许,却不鼓励这种做法(符合底层系统语言的特性,如C)。虽然可以嵌套文件和文件夹,但会带来导入摩擦。核心问题是:这样做真的能提升可读性吗? 理论上拆分能增强可读性,实践中将相关代码整合到单个大文件后,通过分段浏览反而更高效。Zig倾向引导扁平化组织。若模型需配套文件,我直接创建相邻的`model_`文件继续推进。 我认为这种模式不适用于大型项目——终究需要层级结构——但Zig需要层级的阈值比想象中高得多。在Rust中我习惯早早建立目录结构,而在Zig中我不断推迟,直到项目结束都未使用层级结构。这种对比不仅对Zig有启发,促使我反思:文件组织是项目真实需求,还是习惯使然?这也是衡量项目规模的诚实标准:如果第一天就忍不住建文件夹,或许项目比想象中简单。 以下是实际目录对比: **Rust(`src/`)**: ``` src/ ├── lib.rs ├── parser.rs ├── parser/ │ ├── errors.rs │ ├── macros.rs │ ├── model.rs │ ├── tests.rs │ └── grammar/ │ └── json_path_9535.pest ├── query.rs └── query/ ├── atom.rs ├── comparable.rs ├── comparison.rs ├── filter.rs ├── jp_query.rs ├── queryable.rs ├── segment.rs ├── selector.rs ├── state.rs ├── test.rs └── test_function.rs ``` **Zig(`src/`)**: ``` src/ ├── root.zig ├── parser.zig ├── model.zig ├── model_query.zig └── query.zig ``` ## 测试 暂不讨论RFC 9535合规性套件,聚焦语言本身: 在Rust中,我通常采用两种测试方式: - **内联单元测试**:与被测代码同文件/同目录,便捷的默认选项,无需额外配置 - **集成测试**:位于主源码树外的独立目录(如`tests`),作为特例存在 我曾预期Zig有类似划分。表面上相似:可在同文件中编写测试。但问题在于冗余度——鉴于已采用扁平结构,我面临两种选择:为每个模型创建独立`model_test`文件,或直接内联到模型文件。两种方式都会导致混乱(无论是分散文件还是整体目录)。我选择后者,并在`build.zig`中显式配置。连接成功后,测试运行流畅且整洁。 总体而言:我认为Rust编写和管理测试更简便。但Zig的额外摩擦主要源于语言特性,更接近命令式世界的原生模式,而非测试基础设施本身。 ## 无函数式范式 Rust虽是命令式语言,却大量借鉴函数式概念:零成本迭代器、惰性求值、代数数据类型、模式匹配、单子类型、trait、闭包等。受Haskell和Erlang影响,我已深度倾向函数式风格,这在本库中尤为明显。 关键特征包括: - 通过`Queryable`等组合子实现单子式错误控制 - 支持`map`/`flat_map`/`reduce`的单子式数据类型`Data` - 纯粹的不可变转换 - 组合子替代循环 - 闭包实现局部抽象 - 声明式宏作为微型DSL - 和类型与积类型 我明知无法将全部特性移植到Zig,但仍期望保留核心理念。实践中Rust依赖不可变性和组合子时,Zig引导我采用就地变更和命令式原生模式。 **两者接近处:和类型** Rust中简洁直接: ```rust pub trait Query { fn process<'a, T: Queryable>(&self, state: State<'a, T>) -> State<'a, T>; } impl Query for Segment { fn process<'a, T: Queryable>(&self, step: State<'a, T>) -> State<'a, T> { match self { Segment::Descendant(segment) => segment.process(step.flat_map(process_descendant)), Segment::Selector(selector) => selector.process(step), Segment::Selectors(selectors) => process_selectors(step, selectors), } } } ``` Zig中采用鸭子类型: ```zig pub fn query(node: anytype, iteration: *JsonPathIter) !void { const T = switch (@typeInfo(@TypeOf(node))) { .pointer => |p| p.child, else => @TypeOf(node), }; if (!@hasDecl(T, "query")) { return; // 无编译期trait,仅检查方法是否存在 } try node.query(iteration); } ``` **递归模式两者相似** Rust示例: ```rust fn process_descendant(data: Pointer) -> Data { if let Some(array) = data.inner.as_array() { Data::Ref(data.clone()).reduce( Data::new_refs(/* 子元素 */).flat_map(process_descendant) ) } else { Data::Nothing } } ``` Zig示例: ```zig fn collectDescendants(allocator, value: *std.json.Value, path, out) !void { try out.append(allocator, .{ .json = value, .path = try allocator.dupe(u8, path) }); switch (value.*) { .array => |arr| for (arr.items) |*elem| try collectDescendants(allocator, elem, child_path, out), else => {}, } } ``` 但Zig很快迫使你偏离函数式风格,主要因为需直接处理内存分配器,而真正的纯函数式意味着不断构造新结构——要么内存消耗巨大,要么需大量手工簿记避免开销。 **核心差异:变更 vs. 不可变单子** Rust的简单单子变换: ```rust pub fn flat_map(self, f: F) -> Data<'a, T> { match self { Data::Ref(data) => f(data), Data::Refs(v) => Data::Refs(v.into_iter().flat_map(...).collect()), _ => Data::Nothing, } } ``` Zig转向变更: ```zig pub fn queryName(name: []const u8, iteration: *q.JsonPathIter) !void { while (i < iteration.cursors.items.len) { if (obj.getPtr(name)) |val| { iteration.cursors.items[i] = .{ .json = val, .path = new_path }; } else iteration.remove(i); } } ``` **归约 vs. 分支** Rust: ```rust selectors.iter().map(|s| s.process(step.clone())).reduce(State::reduce) ``` Zig: ```zig var lhs_branch = try iter.fork(); defer lhs_branch.deinit(); ``` **组合子 vs. 循环** Rust: ```rust items.iter().enumerate().filter(|(_, i)| cond(i)).map(|(idx, i)| Pointer::idx(i, path, idx)).collect() ``` Zig: ```zig while (i < cursors.len) { if (actual_index < arr.items.len) { cursors[i] = .{...}; i += 1; } else iteration.remove(i); } ``` 总体而言,这反映了各自的设计目标和目标领域,是合理的权衡。但主观上,我认为Zig生成的代码可读性低于Rust对应版本。 ## 内存分配器 内存分配器无处不在:几乎每个函数都接受一个;每个结构体持有一个。这种显式性一旦被接受,遵循规则相对直接。这近乎语言的标志性特征,故无人未被警告。 实践中,过程却繁琐:必须严格遵循初始化/反初始化规范,且调用栈变长时纪律容易松动。这比C中无声段错误或内存损坏有明确改进,但来自Rust的开发者仍需手动执行规则——每次分配资源、处理失败路径、决定反初始化责任方。 幸运的是,Zig的`TestAllocator`可挽救局面。它不会自动捕获所有问题,仍需编写测试用例覆盖失败路径——但一旦完成,可靠性较高。陷阱在于:纸面上看似简单,直到代码复杂化,这些错误便纠缠隐藏。 以下是最典型的案例,对比Rust处理方式: ### 内存泄漏:遗忘反初始化 ```zig var iter = q.JsonPathIter.init(&root, std.testing.allocator); try iter.append(&root, "$['a']"); // 错误:未调用iter.deinit() ``` **捕获方式**:`MemoryLeakDetected`,指向`append`中的`dupe`调用 **修复方法**:初始化后立即添加`defer iter.deinit();` **Rust对应**:`Drop`在作用域结束时自动运行,故此特定错误不存在(注:技术上仍可能泄漏,如`Rc`循环引用或`Box::leak`,但需刻意触发) ### 内存泄漏:错误路径跳过反初始化 ```zig fn build(json: *Value, a: Allocator) !q.JsonPathIter { var iter = q.JsonPathIter.init(json, a); try iter.append(json, "$['a']"); // 正常 try iter.append(json, "$['b']"); // 失败 -> iter泄漏 return iter; } ``` **捕获方式**:`FailingAllocator{ .fail_index = 1 }`迫使第二次append触发`MemoryLeakDetected` **修复方法**:初始化后添加`errdefer iter.deinit();` **Rust对应**:彻底解决。`Drop::drop`在任何作用域退出(包括`?`提前返回)时无条件执行 ### 内存损坏:重复调用反初始化 ```zig fn runQuery(...) !q.JsonPathResult { var iter = q.JsonPathIter.init(json, a); errdefer iter.deinit(); try q.query(qstr, &iter); return iter.toResult(parsed); // 所有权转移给调用者 } fn cacheAndLog(...) !void { var result = try runQuery(json, qstr, a); try cache.append(result); // cache持有result指针的浅拷贝 defer result.deinit(); // 错误:释放cache仍指向的堆数据 printResults(&result); } fn processAll(...) !void { var cache = std.ArrayList(q.JsonPathResult).init(a); defer { for (cache.items) |*r| r.deinit(); // 释放cacheAndLog层已释放的内存 cache.deinit(); } for (queries) |qs| try cacheAndLog(json, qs, a, &cache); } ``` **捕获方式**:使用`std.testing.allocator`运行时,在第二次查询的`cache.items[0].deinit()`(`processAll`清理阶段)失败,`DoubleFree`指向两个释放位置,确认这是跨函数所有权错误而非单行拼写错误 **修复方法**:仅单一层级可拥有值所有权。因`cache`生存期长于`cacheAndLog`,所有权属第三层;第二层移交后不得`defer deinit`: ```zig fn cacheAndLog(...) !void { var result = try runQuery(json, qstr, a); printResults(&result); // 先使用 try cache.append(result); // 再移交所有权——此后无defer } ``` **Rust对应**:此结构无法编译。`cache.push(result)`移动`result`后,`result`作为有效绑定不再存在,故无法意外调用`drop(result)` ### 内存损坏:结构体移动失败时的孤岛分配 ```zig pub fn appendBuggy(self: *Iter, v: *Value, path: []const u8) !void { const duped = try self.allocator.dupe(u8, path); // 错误:无errdefer try self.cursors.append(self.allocator, .{ .json = v, .path = duped }); } ``` **捕获方式**:`FailingAllocator{ .fail_index = 1 }`使数组扩容失败(第二次分配),导致`duped`(首次分配)孤立。此泄漏方式不同于案例一:`iter.deinit()`正常运行,但永不包含此特定字符串 **修复方法**: ```zig const duped = try self.allocator.dupe(u8, path); errdefer self.allocator.free(duped); // 仅当下方append失败时触发 try self.cursors.append(self.allocator, .{ .json = v, .path = duped }); ``` **Rust对应**:构造上保证安全。`Vec::push(item)`移动`item`入列,要么成功要么OOM中止。标准库无可能返回"已分配但未链接"值的fallible push,故根本不存在`errdefer`需填补的缺口 ## 库与核心API 生态系统仍处于早期阶段。库资源稀缺,即使是正则表达式这类基础库也未完全成熟(例如`mvzr`)...

相似文章

重返Zig

Lobsters Hottest

作者描述了从Zig到Rust再回到Zig的历程,探讨了编程语言中稳定性与表达力之间的权衡。

2026 年的 Zig 与 Rust

Lobsters Hottest

本文在 2026 年的背景下对比了 Zig 和 Rust,认为编程代理通过自动化生成 Rust 代码,削弱了 Zig 在人机交互体验上的优势。

以Zig风格构建你的项目

Lobsters Hottest

作者详细介绍了构建一个名为bygge-zig的工具,该工具使用Zig构建系统来编译Rust项目,用更少的代码行复制了Cargo的功能,并突出了其中的差异和挑战。

我们的Rust到Zig重写进展如何

Lobsters Hottest

Roc编译器团队已将他们30万行Rust代码库重写为Zig,经过18个月实现了功能对等,生成了更小的WebAssembly二进制文件并提高了性能。

Zig 示例教程

Hacker News Top

通过带注释的示例,对 Zig 编程语言进行实践性介绍,涵盖从基础到高级的主题。灵感来源于 Go by Example。