Page 184 - 《软件学报》2025年第8期
P. 184

张卓若 等: 面向   Rust 语言的形式化验证方法研究综述                                                 3607


                 量的可行域和作用域, 默认从声明变量开始有效直到变量所在域结束.
                    基于上述所有权规则, Rust 针对不同数据类型实现了相应的内存管理策略. 具体来说, 栈基础数据类型                            (整型、
                 浮点型等) 赋值时自动进行值拷贝           (copy), 因此原变量保持有效. 而堆分配类型          (如  String、Vec<T>) 默认使用移
                 动  (move) 语义. 如图  1  所示, 赋值操作会转移所有权并使原变量失效. 如需确保堆分配变量在赋值操作后保持双
                 重有效性, 需显式执行堆数据的深拷贝            (clone). 智能指针  Box<T> 针对其指向的分配在堆上的         T  类型数据实现内
                 存的独占式管理, 当      Box<T> 的所有权被移动或其作用域结束时, 会自动调用析构函数释放堆上分配的内存, 有效
                 防止内存泄漏. 此外, 当变量作为参数传入函数时, 它遵循与移动操作相同的原则, 即所有权转移到函数中. 相应
                 地, 如果一个变量作为函数的返回值, 其所有权会随着返回语句从函数中移动出来, 转移到调用者的作用域.

                                         1  let x = String::from("hello"); // x  是 String  类型所有者
                                         2  let y = x; // x  的所有权被转移给了 y
                                         3  // println!("{}", x); //  编译错误: x 不再有效
                                         4  println!("{}", y); //  正常: y 是 String  的新所有者
                                                  图 1 Rust 中的所有权机制


                 2.1.2    借用检查
                    所有权系统为      Rust 提供了内存管理的基础, 但有时我们需要引用一个值而不转移所有权. Rust 通过借用
                 (borrowing) 机制实现了这一点. 在    Rust 中, 借用有两种类型: 不可变借用       (immutable borrow) 和可变借用  (mutable
                 borrow). 借用机制允许一个值在同一时间被多个地方使用, 但同时只能有一个可变借用或者多个不可变借用, 且
                 所有借用必须在其创建的作用域结束之前有效, 以此保证了数据的一致性, 防止了数据竞争和悬挂指针. 该程序属
                 性由  Rust 的借用检查器    (borrow checker) 保证, 不满足该属性的程序会被拒绝通过编译. 如图           2, r1  和  r2  是  String
                 对象的不可变借用者, r3      是  String  对象的可变借用者, 当第    6  行试图同时使用    r1, r2  和  r3  时, 编译器会报错, 因为
                 在同一时间不允许既有不可变借用又有可变借用.

                            1  let mut s = String::from("Hello");
                            2  let r1 = &s; //不可变借用
                            3  let r2 = &s; //  另一个不可变借用
                            4  println!("{} and {}", r1, r2); //  同时使用两个不可变借用
                            5  let r3 = &mut s; //词法生命周期下报错: 同时存在不可变借用和可变借用; 非词法不报错
                            6  //println!("{},{} and {}", r1, r2, r3); //(词法和非词法下均)报错: 同时存在不可变借用和可变借用
                            7   println!("{}", r3);
                                                 图 2 Rust 中的借用规则例      1

                    Rust 借用检查器在以下条件同时成立时, 会判定程序中的某个语句违反了借用检查规则: (1) 该语句访问了某
                 个内存对象. Rust 中将内存对象表示为路径           (path) P  的形式, 如局部变量  a, 访问字段  a.b, 解引用操作*a.b; (2) 访问
                 路径  P  违反了由某个借用表达式产生的借用            L (loan); (3) 借用 L 此时是活跃  (live) 的, 意味着 L 未来将被用以访
                 问其借用的内存对象.
                    例如, 在图   3  中所示的悬垂指针问题中, 涉及的事件序列如下: 在第              5  行, 字符串变量   s 退出其作用域, 编译器
                 自动插入的    drop  函数随之释放了与之关联的内存对象, 这一过程相当于对路径                    s 执行了一次写操作. 在第       4  行,
                 一个借用表达式创建了一个借用           L0, 该借用是对    s 的不可变借用. 根据     Rust 的借用规则, 写入   s 与借用  L0  是冲突
                 的, 因为在不可变借用的有效期内, 不允许对借用的内存对象进行修改. 到了第                       7  行, 由于  L0  被使用, 这表明在第
                 5  行执行  drop(s) 时, L0  仍然是活跃的. 因此, 借用检查器发现了这一借用违规, 并报告了错误.
                    给定一个程序, 能够确定哪些语句将访问内存对象, 以及这些访问操作与借用规则之间的冲突. 关键问题在于
                 准确推断各个借用的活跃区域, 即           Rust 语言中所谓的生命周期. Rust 通常能够通过借用检查器自动推断生命周
   179   180   181   182   183   184   185   186   187   188   189