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 标志运行试用.
   180   181   182   183   184   185   186   187   188   189   190