配了三年 Rust,每次 match 里的 get_mut 都要被编译器误杀——Polonius 把这件事彻底变了
配了三年 Rust,有个代码写法我从来不敢写——match 两个分支,一个读 map,一个写 map,编译器说”借用冲突”。我以为是代码写错了,其实是 NLL 这个借用检查器太蠢了,它把你 return 出去的借用当成在整个函数里都活着。
今天这件事被 Polonius 彻底修了。
这个坑你一定踩过
这个代码,Rust 编译器报错了三年:
fn get_mut_or_default<K, V>(map: &mut HashMap<K, V>, key: K) -> &mut V {
match map.get_mut(&key) {
Some(value) => value, // NLL 认为这个借用活到函数末尾
None => {
map.insert(key, V::default); // 报错:和上面的借用冲突!
map.get_mut(&key).unwrap()
}
}
}
这是 Rust 程序员最常遇到的无辜报错之一。逻辑上,两个分支是互斥的——有值就返回,没值就插入然后再返回。借用检查器应该能理解这件事。但 NLL 不理解。
原因是 NLL(Non-Lexical Lifetimes)是「流不敏感」的(flow-insensitive)。它只看变量的作用域,不看控制流。value 这个借用在第一个分支出现,NLL 就认为它活到函数末尾。于是第二个分支里的 map.insert() 变成了”在借用还在的时候写入”——报错。
Polonius 是什么
Polonius 是 Rust 团队花了多年开发的下一代借用检查器。核心区别:它是「流敏感」的(flow-sensitive)。
流敏感意味着 Polonius 知道 value 只在 Some 分支里存在,一旦进入 None 分支,那个借用就已经结束了,不影响后面的写入。
同样一个函数,Polonius 能正确理解两个分支是互斥的,允许编译通过。
除了互斥分支这个经典场景,Polonius 还修了其他几类 NLL 误报:
- 条件 return 里的借用
- 循环里提前退出的借用
- 更复杂的跨语句依赖分析
性能会不会炸?
这是最实际的担心。更精准的检查,意味着更多的计算。Rust 官方对 crates.io 下载量前 10000 的开源库做了全量基准测试:
- 绝大多数 crate:编译性能和 NLL 基本持平
- 少量借用密集型代码:最高可能出现 2-3 倍编译耗时增加
- 整体来看:大多数项目感知不到差别
对于踩到这个坑的团队,可以临时用 -Zpolonius=off 禁用,等官方优化后再开。或者在 CI 里单独测编译时间。
现在怎么用
Polonius Alpha 已经默认开启在 Rust nightly 上。稳定版预计今年年底发布,届时会直接替代 NLL 成为默认借用检查器。
已有的代码不需要任何修改。Polonius 100% 兼容 NLL 允许的所有代码,不会有任何 breaking change。
对实际项目的影响
这个升级对业务代码的影响因人而异:
原本就不踩这个坑的团队:迁移收益几乎为零,就是编译速度快了 8%(官方数据),但不会有感知。
经常写复杂借用逻辑的团队:这是最大的受益者。那些被 Rc<RefCell<T>> 绕过的场景,现在可以直接写正常的 Rust 代码。
写 async 库的团队:Polonius 对 async/await 场景下的借用分析有特别优化,这类代码收益最明显。
结论
Polonius 不是一个新功能,它是 Rust 编译器对「什么样的借用是安全的」这件事的理解升级。从今年年底开始,你写的代码不需要变,但编译器能放行的正确代码变多了,误报变少了。
配了三年的那个 get_mut_or_default 写法,从今天起可以直接写了。
评论区
登录后可评论。