配了三年 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 写法,从今天起可以直接写了。

评论区

0 条评论

登录后可评论。