强类型思维网

这是作者对 Zig 语言使用数月后的思考总结,包含喜欢和不喜欢的方面:

  • 喜欢(What I like)

    • 任意大小整数和紧凑结构体(Arbitrary sized-integers and packed structs):支持任意大小整数,如u3,可通过packed struct自动实现类似 C 中按位操作的代码,方便与期望位域的 C 库交互,且无需 CPP 宏等,比 Rust 处理方式更自然。
    • 泛型类型在类型级别就是函数(Generic types are just functions at the type level):在 Zig 中,典型的struct Vec<i32>实际上是一个接受type并返回type的函数fn Vec(comptime T: type) type,这允许更多灵活性,例如通过@sizeOf(T)进行特化,还能利用编译时反射在不依赖中间件的情况下实现基于类型结构的算法,如 JSON 序列化。
    • 错误联合类型(Error Union Types):Zig 的核心错误处理方式,将enum(整数标签)与常规T值胶合在一个标记联合中,要么是错误判别式,要么是T值。错误类型全局声明且结构类型化,可通过||组合创建更复杂的错误类型。与 Rust 相比,其优点是所有错误类型扁平,无需嵌套,可通过 coercion 规则自动转换,如fn foo()!i32 { return 3; },无需像 Rust 那样用Ok(())包装成功路径,try关键字也能方便地传播错误,但存在一些限制,如不能携带值、链式调用不好等。
    • C 互操作性可能是最好的(C interop is probably the best):如果需要大量使用 C 库,Zig 有很好的特性和内置功能,如@cImport / @cInclude可在编译时读取.h文件并将其内容暴露为 Zig 符号,还能在comptime时使用inline forc的内容进行转换。
    • 构建系统很好(The build system is nice):项目的构建配置用 Zig 编写,虽然作者不喜欢“配置即代码”的方式,但它能让习惯 CMake 等工具的人感到新鲜,Zig 构建模块理解起来不复杂,在配置目标、优化、CPU 架构等方面有很大灵活性,但当前依赖处理远不如cargo等工具。
  • 不喜欢(What I like less)

    • 错误处理(Error handling):Zig 的错误处理虽有特点,但缺乏携带值的功能,导致在处理错误时需要额外的代码基础设施,如确定具体哪个文件未找到,且不能像 Rust 的Result<A, E>那样方便地处理错误和值。try关键字也有局限性,链式调用不好,对于返回?A(可选)的函数处理不够简洁。
    • 禁止重命名(Shadowing is forbidden):Zig 不支持重命名,这在处理错误处理时会带来不便,如一系列链式调用中需要为每个变量起不同的名字,否则可能会导致混淆。
    • 编译时鸭子类型(Compile-time duck typing)comptime关键字在 Zig 中无处不在,但它存在问题,如无法确定函数的输入要求,标准库中的函数文档缺失,导致代码和文档容易不同步,增加了出错的风险,尤其在软件成熟后修改内部实现时。
    • 没有类型类/特质(No typeclasses / traits):Zig 缺乏特质,这影响了编程契约的传达和版本控制,程序员无法通过特质明确知道函数的输入要求和可调用性,也难以构建基于特质的实现列表,与 Rust 相比,在接口设计和代码维护方面存在不足。
    • comptime可能不像看起来那么有趣(comptime is probably not as interesting as it looks)comptime反射虽然不错,但由于缺乏特质,存在隐藏约束,导致无法准确理解函数的契约、前置条件等,文档和代码可能不同步,增加了理解和维护的难度,感觉像 C++模板,社区也不太可能解决这个问题。
    • 没有封装(No encapsulation):Zig 没有封装,使用类型如ArrayList(i32)时,必须知道其实现细节才能获取有用的函数和属性,文档也不能提供足够信息,甚至可以修改内部状态,虽然有模块级别的访问控制,但与 C 类似,不够完善。
    • 内存安全被高度低估和错误(Memory safety is highly underestimated and fallacious):Zig 并不比unsafe Rust 更安全,虽然 Zig 有一些检测未对齐指针等的机制,但在处理使用后释放(UAF)等问题上存在不足,如示例代码在不同优化选项下输出结果不同,且关于 UB 的处理仍在讨论中,而 Rust 有miri工具可以帮助发现 UB。
    • 延迟编译和编译错误而不是警告(Lazy compilation and compilation errors instead of warnings):Zig 实现了延迟编译,虽然有std.testing.refAllDecls函数来缓解,但在代码重构和处理后期添加代码时可能会带来问题,而且对于未使用的函数参数会拒绝编译,而不是给出警告,难以禁用这种严格的检查。
    • 没有析构函数(No destructors):Zig 没有可靠的资源管理方式,类似于 C,需要调用者在文档中明确提及释放逻辑,defer等工具虽可用于资源清理,但不是自动的,容易被忽略,导致资源泄漏等问题。
    • 没有(unicode)字符串(No (unicode) strings):标准库对 unicode 字符串支持不足,没有专门的字符串类型,建议用户“按字节迭代”,容易导致各种问题和数据损坏,目前仍在讨论中,用户只能在用户空间寻求支持。
  • 结论(Conclusion):Zig 旨在取代 C,追求简单性而牺牲了一些内存安全和可靠性方面的特性。作者认为现在得出 Zig 是否能贡献更可靠软件的结论还为时过早,需要观察使用 Zig 编写的成熟项目。简单性不应是可靠软件的主要目标,而应是在解决问题的基础上尽量使语言简单,Rust 就是一个例子,虽然不简单但在编译时解决了很多问题,比其他方法更简单。作者认为自己在 Zig 上的冒险到此为止,因为对其正确性和可靠性感到失望,也厌倦了“技能问题”文化。
阅读 7
0 条评论