Page 185 - 《软件学报》2025年第8期
P. 185
3608 软件学报 2025 年第 36 卷第 8 期
期, 但在一些复杂的场景中, 开发者需要显式地标注生命周期. 生命周期标注通常以撇号 ('a, 'b) 的形式表示, 并且
出现在引用的类型声明中. Rust 的借用检查器通过建立约束规则来确定借用之间的活跃区域关系, 并求解这些约
束以找到最小活跃区域. 这种静态分析方法得到的活跃区域是对实际情况的过度近似, 目的是确保只有遵守借用
规则的程序才能被编译通过, 这同时也导致了一些实际上正确的程序可能被错误地拒绝.
1 let r: & String;
2 {
3 let mut s = String::from("hello");
4 r = & s; // r 借用 s
5 /* drop(s) 由编译器自动插入*/ // s 离开其作用域, 值被回收, 悬垂指针编译报错
6 }
7 println!("{}", r); // 引用 r 在这里继续使用
图 3 Rust 中的借用规则例 2
Rust 借用检查器的发展经历了 3 个阶段: 从早期的词法生命周期, 到目前采用的非词法生命周期, 以及即将引
入的 Polonius. 每一个阶段的进步都旨在提高活跃区域推断的精确度, 从而减少对合法程序的误报, 使得更多符合
规范的程序得以成功编译.
词法生命周期基于源代码的词法作用域 (lexical scope) 来推断生命周期. 在词法生命周期下, Rust 编译器严格
按照代码块的结构来决定引用的有效范围. 这种推断方式虽然简单直接, 但过于保守. 如图 2 中, 第 2 行的借用表
达式创建了路径 s 的不可变引用 L0, 并且赋给了 r1, 在词法生命周期的推断下, L0 的生命周期被认为直到代码块
结束前都是活跃的, 因此在第 5 行试图创建可变引用 r3 时与 L0 相悖, 编译器会报错. 虽然在实际运行中 r1 在第
4 行之后不再被使用.
为了改善词法生命周期的保守性, Rust 团队引入了非词法生命周期推断 (NLL). 在 NLL 模式下, 不再依靠词
法作用域来推断借用的生命周期, 而是基于程序的控制流图. 即, 借用的生命周期被表示为控制流图上程序点
(program point) 的集合. 如果一个生命周期包含了点 P, 这意味着具有该生命周期的引用在进入 P 时是有效的. 如
图 4 所示, 右边是一个示例程序, 左边是对应的简化了的该程序控制流图. 首先通过活跃变量分析可以得到引用变
量 p 的生命周期'p 为'foo 和'bar. 右图第 4 行程序语句生成约束'foo: 'p @ A1, 这表示在程序点 A1 处, 'foo 会包含 'p
从 A1 点可达的程序点. 于是, 'foo={A1, B0, C0}, 同理, 由第 7 行生成约束'bar: 'p @ B2 得到'bar={B2, C0}. 根据非
词法生命周期, 图的示例可以不需增加词法块直接通过借用检查. 在 NLL 的推断下, 面对同样图 2 的代码编译器
会发现 r1 和 r2 在第 4 行之后不再被使用, 因此可以在该点结束 r1 和 r2 的生命周期, 从而允许创建 r3 的可变借
用, 这种改进极大地提升了 Rust 代码的灵活性和易用性.
[17]
Polonius 是 Rust 未来的借用检查引擎, 它不再使用生命周期, 即为每一个借用计算出可能覆盖的程序点, 而
是去维护一个借用来源 (Origin) 的集合 (后文用“区域 (Region)”指代), 直接反映数据流向关系. 如果一个区域包含
了某个借用并且该区域根据活跃变量分析在程序点 P 处活跃, 就判定该借用在该程序点是活跃的. 对于图 4 的程
序, 其右图第 4 行和第 7 行程序语句分别创建了两个借用 L0 和 L1, 区域为'foo={L0}和'bar={L1}, 根据第 4 行生
成约束'foo: 'p @ A1, 这表示'foo 是'p 的子集, 于是'p={L0}. 第 7 行处的约束使'p='p∪{L1}–{L0}={L1}, 并最终
在 C0 处合并为'p={L0, L1}, 所以 L0 在{A1, B0, C0}活跃, L1 在{B2, C0} 活跃, 这和非词法生命周期是一致的.
Polonius 不仅可以接受所有 NLL 可以接受的程序, 对于其保守拒绝的一些情况也能有效处理, 如图 5 所示的返回
借用, 在非词法生命周期下, 由于行 3 的 v 会被返回给调用者函数, 其生命周期会包含 get_or_insert 函数所有的程
序点, 导致行 2 的不可变借用 L0 的生命周期也覆盖了 None 分支里面的所有程序点, 于是行 5 创建可变借用的时
候就会冲突报错; 而在 Polonius 的视角, v 的区域在 None 分支并不活跃, 所以 L0 在 None 分支里也不是活跃的,
不会冲突. Polonius 获取到程序相关事实 (facts) 后, 会转交给 Datalog 来做推理, 以提升效率. Polonius 已集成到
nightly rustc 中, 可以使用该-Zpolonius 标志运行试用.

