你刚写完一个新功能,兴奋地敲下 cargo build。终端跳出一屏 borrow checker 错误。第 N 次,&mut 和 Rc<RefCell<T>> 之间的纠结让你怀疑人生:为什么不能像写 Swift 那样随手 var,又不需要担心运行时崩溃?
这是 Rust 程序员熟悉的痛感。borrow checker 保证内存安全,但代价是陡峭的学习曲线和无处不在的所有权约束。而在路的另一边,Swift 的 ARC(自动引用计数)让开发者几乎不用想内存管理,但每一次 retain/release 都是运行时开销,且在复杂引用循环面前仍需 weak 手动救场。
长期以来,这两条路线被视为互斥的——要么编译期零开销但心智负担重,要么运行时自动管理但有一定代价。Ante 语言正在尝试打破这个二分法。
两者为什么难以融合?
Rust 并非没有尝试过。它的 Rc 类型提供了引用计数,但当你需要在多个地方修改共享数据时,必须加上 RefCell,写成 Rc<RefCell<Spaceship>>。RefCell 在运行时会检查借用规则——如果两个调用者同时拿到可变引用,程序直接 panic。try_borrow_mut() 可以避免崩溃,但只是把问题转移到别处:调用者必须处理 None 返回。
Swift 也往这个方向走了。它引入了 borrowing system,但依赖昂贵的运行时排他性检查。同样,一旦规则被违反,程序在运行时崩溃。
核心矛盾在于:borrow checking 要求「同一时刻最多一个可变引用」;RC 则天然允许多个持有者共享同一份数据。两者在逻辑上就是对冲的。任何融合方案都必须解决同一个问题——当多个地方持有对同一数据的引用时,如何保证某个位置的可变操作不会让其他位置的引用变成悬垂指针?
Ante 的答案:shape-stability
Ante 是一个尚在开发中的系统编程语言,定位是「更简单的 Rust」。它引入了 shape-stability(形状稳定性)概念:如果一个类型的「形状」在运行中不会改变——即它占用的内存不会被释放或重新解释——那么对它的引用就是安全的,无论别处对它做了什么修改。
换句话说,只要一块数据不可能被销毁,你就可以持有任意多个指向它的可变引用——因为没有悬垂的风险。
来看一个例子:
type Entity =
energy: I32
health: I32
heal (healer: mut Entity) (target: mut Entity) =
healer.energy -= 10
target.health += 10
在 Ante 中,你可以用同一个 Entity 同时作为 healer 和 target 调用 heal:
self_heal (entity: mut Entity) =
heal entity entity
两个 mut 引用指向同一块内存,编译器却不会报错。理由很简单——Entity 的内部结构(两个 I32 字段)是形状稳定的。修改字段的值不会改变结构的形状,也不存在释放 Entity 的操作,两个引用始终合法。
更复杂的情况同样适用:你可以同时持有指向 Spaceship 的一个可变引用,和指向其 engine 字段的另一个可变引用:
refuel (ship: mut Spaceship) =
engine_alias: mut Engine = ship.engine
ship.engine.fuel := 200 // 使用原始的 ship 引用
engine_alias.fuel := 100 // 使用 engine 别名
在 Rust 中这无法编译——ship 已经通过 ship.engine 产生了可变借用,不能再同时用于赋值。在 Swift 中同样不行。Ante 的编译器理解:只要 ship 本身不被销毁,它的所有子字段也都安全。这正是 shape-stability 赋予的自由度。
引入引用计数:shared 和 Rc
shape-stability 真正发光的地方,是它和引用计数的结合。
在 Ante 中,给类型定义加上 shared 关键字,就启用了自动引用计数:
shared mut type Spaceship =
engine: Engine
name: String
launch (var ship: Spaceship) =
set_fuel (mut ship.engine)
set_fuel (engine: mut Engine) =
engine.fuel := 100
launch 拿到一个引用计数的 Spaceship,然后创建了一个指向其 engine 字段的可变引用并传给 set_fuel。这个操作在 Rust 中需要经过 RefCell::borrow_mut()(可能 panic),在 Swift 中需要经过运行时排他性检查(可能崩溃)。Ante 的编译器在编译期就能证明这个操作是安全的:launch 持有 ship 的引用,所以 ship 不会在 set_fuel 执行期间被释放。
Ante 的核心洞察在于:只要有一个持有者保持对象存活,对对象内部的可变借用就是安全的。这个逻辑和 Rust 的 borrow checker 一脉相承,但 Ante 将其扩展到了引用计数场景——编译器追踪的核心问题是「有没有人保证这块数据不会被销毁」,而非传统 borrow checker 关注的「谁拥有这块数据」。
处理 union:uniq 机制
shape-stability 能安全地处理 struct,但 union(和类型/tagged union)是另一回事。因为 union 的 variant 可以在运行时改变——一个 Option<String> 可以从 Some("hello") 变为 None,原先的 String 被释放——这就是悬垂引用的根源。
Ante 为此引入了 uniq(独占可变引用)和临时转换机制。uniq 类似于 Rust 的 &mut:它保证在当前作用域内,没有其他代码可以访问指向同一数据的引用。
launch (var ship: Rc Spaceship) (var other_ship: Rc Spaceship) =
match uniq ship.engine
| StringTheoryEngine str ->
other_ship.engine := ImpulseEngine 0x42
str.[0] := 'z'
| ImpulseEngine fuel ->
()
如果 ship 和 other_ship 指向同一个 Spaceship,这段代码会导致非法访问——在拿到 str 引用之后,重新赋值 other_ship.engine 会销毁 StringTheoryEngine,使得 str 变成悬垂指针。Ante 编译器会拒绝这段代码。
Ante 的做法和 Rust 有一个微妙但关键的区别:Rust 通过所有权系统确保在整个程序的生命周期中,任何时候都不会有两个可变引用同时存在;Ante 则只要求在当前作用域内没有其他代码访问可能冲突的引用。这是一种局部约束而非全局约束,更接近 C 语言中 restrict 指针的思路——可以有多个指针指向同一块内存,但编译器只保证在某段代码中只有一个被使用。
代价与权衡
这种方案并非没有代价。Ante 的创建者 Jake 也坦言,当前的 type analysis 有一定局限性:要回答「Engine 类型是否可能包含一个 Spaceship 的引用」这种问题,编译器必须递归检查整个类型图。这意味着给 struct 添加一个字段可能成为破坏性 API 变更——对于库作者而言,这比 Rust 的约束更隐蔽。
目前团队正在探索几个改进方向:用匿名品牌(anonymous brand)标记引用计数类型以替代类型分析;引入类似 Pony 的 iso 隔离权限;或者通过 effect system 追踪可变性来源。
社区对此也有质疑。HN 上有评论指出,允许多个可变引用共存虽然在单线程场景下内存安全,但在多线程环境下可能导致 data race——即使对简单的 I32 字段的并发写入,在没有原子操作的情况下也可能产生未定义行为。Ante 的单线程安全性和多线程安全性之间的界限还有待明确。
格局正在松动
这篇文章的作者 Evan Ovadia(Vale 语言的创建者)提出了一个有趣的观察:我们曾经认为「共享可变借用」是不可能的——Rust 基本建立在这个信念之上。但越来越多的反例正在打破这个教条:
- Ante 通过 local uniqueness 在共享可变数据上获取独占借用
- Vale 通过 pure function 在共享可变数据上获取不可变借用
- Group borrowing 允许编译器对非形状稳定的数据创建共享可变借用
- Rust 的 GhostCell 让对象图可以互相引用
这些技术指向同一个方向:有一条更统一的内存安全理论正浮出水面,只是我们还没完全看清它的轮廓。
意义
Ante 目前仍是一个早期项目,部分特性已实现,部分仍处于理论阶段。它的价值在于提出了一个值得认真对待的问题:borrow checking 和引用计数真的是二选一吗?
如果这条路走通了,其影响将是深远的。开发者可以先用引用计数快速原型,再逐步将热点路径迁移到 borrow-checked 代码——不需要重写,不需要在两种范式之间手动搭桥。这种渐进性是现有语言都不具备的。
Rust 证明了零开销抽象是可能的。Swift 证明了自动内存管理可以是实用的。Ante 瞄准的是那个更难的命题:两者是否可以兼得?即使最终答案是否定的,这个问题本身就值得被认真追问。
本文的素材来自公开信息和社区讨论。如果你对这个话题有更深入的一手经验,欢迎指出文中的不足。
参考链接
- Ante: A New Way to Blend Borrow Checking and Reference Counting — Evan Ovadia 的原文
- HN 讨论帖
- Ante 语言官网
- Borrow checking, RC, GC, and the Eleven (!) Other Memory Safety Approaches — Verdagon 对 13 种内存安全方案的全面梳理
- Memory Management in Rust and Swift — Ryan Levick 对两种路线的对比