Zig、Rust 与其他语言

Zig 的分配模型与“没有隐藏分配”

  • 多条评论质疑文章中关于 Zig 避免隐藏分配的强烈说法。
  • 标准 C 函数大多不会分配;strdup 是一个明显例外。
  • 在 Zig 中,大多数 stdlib API 遵循“如果这可能分配,就传入 allocator”的惯例,但并非全部如此:
    • std.Thread.spawn 会分配(线程栈 + 参数负载的堆内存),但没有显式的 allocator。
    • std.ArrayList 在内部保存 allocator,因此即使没有传入 allocator,方法调用也可能分配。
    • ArrayListUnmanaged 的存在是为了把 allocator 保持在更高层级,并让分配显式化。
  • 一些人认为这“只是一种惯例”,而不是严格保证;另一些人则认为这依然很有价值。

不同语言之间的内存管理权衡

  • Nim 被强调为一种内存管理可配置的系统语言;默认的 ARC 被认为比在 Zig 代码中层层传递 allocator 更符合人体工学。
  • 反方观点:在 Zig 中也可以使用全局 allocator,或将 allocator 附着到对象上,以避免过多样板代码。
  • Odin、带有良好 API 和 arena 的 C,以及其他语言都被拿来说明:API 设计比语言本身更重要。

标准库规模 vs 生态碎片化

  • 一派更偏好紧凑的 stdlib,以避免 C++ 式膨胀、促进更快演进,并减少过时 API 的长期维护成本。
  • 另一派认为,小 stdlib 会迫使人们依赖大量第三方包,导致碎片化、多个相互竞争的库,以及依赖地狱。
  • 示例:
    • Python:通常认为大 stdlib 是优点,尽管部分模块已经过时;但流行的第三方库(例如 HTTP 客户端)在易用性上仍胜出。
    • Go:stdlib 受到称赞,但具体部分(日志、flags、更早期的 net.IP)受到批评;较新的抽象如 netip 改善了这些问题。
    • Rust:stdlib 中没有压缩功能被辩护为把不断演进的领域留给 crates;但也有人指出其他生态的 stdlib 里有很好的压缩器,作为反证。
  • 建议的折中方案:小而核心的 stdlib + 官方维护的“认可”包,或者在后期采纳事实标准。

包管理器与依赖问题

  • Zig 的包生态:
    • 通过 build.zig.zon 和 git 引用提供包管理;预计 0.12 的改进会带来提升。
    • 按设计没有中央包仓库(类似 Go);有人认为这更安全,也有人认为这是个糟糕的决定。
  • 一般性担忧:
    • 多个竞争性的 C/C++ 包管理器(vcpkg、conan 等)彼此不兼容。
    • Rust 的 cargo
      • 避免了按项目重复存放源码;死代码消除缓解了二进制膨胀。
      • 被提及的问题:缓存很大、增量构建的旧产物陈旧;夜间版上即将加入的自动缓存 GC 受到欢迎。
    • 有人建议使用 Nix 代替语言专属的包管理器。

字符串、Unicode 与类型系统保证

  • Zig:
    • 使用 []const u8 表示字符串;按惯例这些是 UTF-8。
    • 函数通常会假定有效 UTF-8,而不会在运行时检查;责任由生产者承担。
    • 批评者把这比作“只要别写 bug 就行”,并主张把编码不变量编码进类型里。
  • Rust:
    • 区分 String(拥有的 UTF-8)和 &str(借用的 UTF-8),在安全代码中会进行运行时 UTF-8 验证。
    • 不安全构造函数允许跳过检查,但前提是程序员承诺其有效性。
    • OsString 这样的独立类型避免了在不能保证 UTF-8 的地方强行要求 UTF-8。
    • 这种模型因安全性和灵活性受到赞扬,但也因复杂性和学习成本受到批评。
  • Go 和其他语言:
    • Go 的 string “只是字节”;可以包含任意数据,不保证 UTF-8。
  • Unicode 支持:
    • Zig 的 std.unicode 覆盖了验证和码点迭代;更高级的支持(grapheme、分类)在外部库中。
    • Rust 和 Go 中也存在类似缺口;完整的 Unicode 处理(例如 grapheme 迭代、基于语言环境的排序)很复杂,通常交给库来完成。
  • 有人认为,由于“字符串”的需求差异极大(可变性、切片、Unicode 语义、性能),因此生态中出现多种字符串抽象是自然的,即使存在内建类型。