借用检查器是我在 Rust 中最不喜欢的东西

2010 年代的编程语言中,Rust 可能是最受赞誉的。其主要卖点是能将速度、低级控制与高水平的抗 bug 性(即安全性)相结合,主要创新是借用检查器,可在不增加运行时成本的情况下实现垃圾回收语言的内存安全性。

借用检查器的基本问题

  • 抽象层面,借用检查器需在编译时知道所有引用的生命周期,这不现实,因为生命周期常是运行时属性。
  • 算法层面,它执行的所有权模型过于严格,拒绝了太多行为良好的程序。
  • 实现层面,当前的借用检查器不完整,常拒绝符合所有权模型的程序。
  • 借用检查器的问题往往不易展示,只有在现有项目需要修改所有权结构而被拒绝时才会感受到。

借用检查器失败的例子

  • 如对结构体不同字段的同时可变引用会被拒绝,尽管它们不指向同一数据,这是借用检查器规则实现不准确导致的。
  • 跨函数时,借用检查器可能错误地拒绝代码,即使函数内部逻辑不会相互干扰。
  • 在函数的不同分支中,借用检查器也可能无法正确推理。

“足够智能的借用检查器”

  • 有人认为上述例子是实现限制,未来会改善,如采用非词法生命周期和新的 Polonius 公式能提高准确性,但 Polonius 已研发 7 年仍未完成,且借用检查器永远不会“完整”,因为它要对代码进行推理,而程序难以做到足够深入。

规则本身不便于使用

  • 有时抽象所有权模型的实现存在限制,有时模型本身就不适合程序,如对临时值的引用限制、混合所有权结构的限制等,这些在垃圾回收语言中不是问题,但在 Rust 中是自找的。
  • 有人认为借用检查器的痛苦是“前期痛苦”,能保证内存安全,但实际经验并非如此,很多时候是不必要的限制。
  • 对于经验丰富的 Rustaceans 来说,一些解决借用检查器问题的方法看似简单,但在大规模代码中可能是挑战,而且会导致“逃避现实”,让人更关注满足借用检查器而不是解决实际问题。

为什么不……

  • 解决借用检查器问题的常见方法有使用更少的引用并复制数据(或“克隆”)、使用Rc / Arc / RefCell / Box、使用索引代替引用等,但这些方法都有各自的问题。

Rust 的安全性部分归功于借用检查器

  • Rust 的安全性应归功于其广泛的良好设计和风格,如广泛使用枚举和穷尽模式匹配、使用自定义类型在类型系统中编码信息、强制使用关键字参数构造结构体等,其标准库也有不易误用的 API。
  • 具有类似特征但使用垃圾回收器的语言也能有大部分 Rust 的正确性,如 OCaml 和 Haskell。

好吧,借用检查器也不是“一无是处”

  • 没有借用检查器,要么手动管理内存(繁琐且易出错),要么使用垃圾回收器,后者有延迟和内存效率低的问题,对于非底层应用,这些问题不适用。
  • 比较垃圾回收器和借用检查器的性能并不简单,GC 在某些情况下会导致性能下降,但在某些基准测试中,Julia 的 GC 比 Rust 的确定性销毁更快。
  • 借用检查器能独特地防止一些垃圾回收器无法防止的 bug,如多线程代码中的数据竞争,但在单线程代码中,防止外部持有引用的突变并不常见。此外,借用检查器还有一些意外的小好处,如解锁优化等。

结论

在日常工作中,在 Julia、Python 和 Rust 之间切换时,会怀念各自的优点,但绝对不会怀念借用检查器。

阅读 13
0 条评论