引言
不可变借用是 Rust 借用系统的基础机制,它允许多个读者同时访问数据而不转移所有权。通过 &T 语法创建的不可变引用提供了共享但只读的访问权限------可以读取数据、调用不可变方法、创建更多不可变引用,但不能修改数据或创建可变引用。这种设计源于一个核心原则------共享访问与可变访问互斥,要么多个读者、要么一个写者,绝不允许同时存在。这个原则在编译期强制执行,消除了数据竞争的整个类别------多个线程同时读取是安全的,但读写并发会被编译器拒绝。不可变借用的规则看似简单------可以同时存在多个不可变借用、不可变借用期间原值不能修改、不可变借用的生命周期不能超过被借用值,但实践中的细节复杂且微妙。理解不可变借用的限制------与可变借用的互斥关系、内部可变性的例外、生命周期的传播、借用分割的作用域,掌握编译器的检查机制------借用检查器的流敏感分析、非词法生命周期(NLL)的精确追踪、冻结语义的实现,学会处理常见问题------借用活跃期的重叠、迭代器失效、闭包捕获的限制,是编写正确且优雅的 Rust 代码的关键。本文深入探讨不可变借用的规则、编译器实现和实践中的应用模式。
不可变借用的核心规则
不可变借用的第一规则是共享性------可以同时存在任意多个不可变借用指向同一数据。这种共享是安全的,因为所有借用都是只读的,不会相互干扰。let r1 = &x; let r2 = &x; let r3 = &x; 创建三个不可变引用,都有效且可以同时使用。编译器不限制不可变借用的数量,只要它们不与可变借用共存。
只读性是不可变借用的本质限制。通过 &T 不能修改 T 的内容------不能赋值、不能调用可变方法、不能获取可变引用。这种限制在类型系统层面强制------&T 类型只提供不可变访问的方法。尝试修改会导致编译错误,如 "cannot assign to data in a & reference" 或 "cannot borrow as mutable"。
不可变借用与可变借用互斥是借用系统的关键不变量。当存在不可变借用时,不能创建可变借用;当存在可变借用时,不能创建不可变借用。这个规则防止了别名与可变性同时存在------要么共享但不可变,要么独占且可变。编译器精确追踪借用的活跃期,确保不同类型的借用不会重叠。
生命周期约束保证不可变借用不会悬垂。借用的生命周期必须短于或等于被借用值的生命周期------不能返回指向局部变量的引用、不能在所有者销毁后使用借用。编译器通过生命周期参数和借用检查器验证这个约束,在编译期消除悬垂引用的可能。
冻结语义与修改限制
不可变借用会"冻结"被借用的值------在借用活跃期间,原值不能修改。这种冻结是借用检查器实现的关键机制------确保不可变借用看到的数据保持一致。即使通过原变量名访问,也不能修改,因为存在指向它的不可变引用可能正在使用数据。
冻结是传递的------如果结构体被不可变借用,其所有字段都被冻结。不能通过原变量修改任何字段,即使某些字段看起来没有被借用使用。这种整体性保证了内存安全------部分修改可能破坏不可变借用假设的不变量。
冻结的范围由借用的生命周期决定。非词法生命周期(NLL)让编译器精确追踪借用的最后使用点,而非词法作用域。借用在最后一次使用后结束,原值立即解冻,可以再次修改。这种精确性让代码更灵活,避免了不必要的限制。
内部可变性是冻结规则的受控例外。Cell 和 RefCell 提供了通过不可变引用修改内容的能力,但有严格的限制------Cell 只适用于 Copy 类型,RefCell 在运行时检查借用规则。这种例外让某些设计模式成为可能,如缓存、引用计数,但需要额外小心避免违反借用不变量。
借用检查器的实现机制
借用检查器通过流敏感分析追踪每个借用的活跃期。它不是简单地将生命周期绑定到词法作用域,而是分析控制流的每个路径,确定借用在哪些点是活跃的。这种分析考虑分支、循环、提前返回等复杂控制流,保证安全性的同时提供灵活性。
非词法生命周期(NLL)是 Rust 2018 引入的重要改进。它让借用的生命周期精确到最后一次使用,而非整个作用域。这解决了许多旧版本中的假阳性错误------借用实际上已经不再使用但编译器认为仍然活跃。NLL 让代码更自然,减少了对 drop(r) 等技巧的需求。
借用分割是编译器识别字段级别借用的能力。借用结构体的不同字段不会相互干扰------可以同时不可变借用一个字段、可变借用另一个字段。这种细粒度的追踪让数据结构的操作更灵活,避免了整体借用的过度限制。
编译器的错误信息精确指出违规位置。"cannot borrow x as mutable because it is also borrowed as immutable" 显示不可变借用的创建位置、可变借用的尝试位置、不可变借用的使用位置。这种详细信息帮助程序员理解借用的生命周期,调试借用检查错误。
不可变借用的常见陷阱
迭代器失效是不可变借用的经典问题。在迭代集合时,不能修改集合------迭代器持有对集合的不可变借用,任何修改尝试都违反借用规则。这防止了迭代器失效导致的未定义行为,但需要程序员采用其他模式------收集需要修改的索引、使用 retain 等原地修改方法、或重构避免迭代中修改。
闭包捕获不可变借用可能导致意外的借用延长。闭包捕获变量的引用,即使闭包从不调用,借用也持续到闭包被丢弃。这让原变量在闭包存在期间保持冻结,可能阻止后续操作。解决方案是显式 drop(closure)、使用 move 闭包转移所有权、或重构避免长期持有闭包。
方法链中的借用传播需要特别注意。每个返回 &Self 的方法都延长了借用,整个链条保持一个借用活跃。这在某些模式中有用(如 builder),但可能阻止插入修改操作。理解方法链的借用语义,选择合适的返回类型(&Self vs Self),是设计流畅 API 的关键。
生命周期参数的推导有时令人困惑。编译器按规则推导省略的生命周期,但不一定符合程序员的意图。显式标注生命周期参数、理解省略规则、阅读编译器错误信息,能帮助理解生命周期的实际含义,解决借用检查问题。
深度实践:不可变借用的规则与模式
rust
// src/lib.rs
//! 不可变借用的规则与限制
/// 示例 1: 基本的不可变借用规则
pub mod basic_immutable_borrow {
pub fn demonstrate_multiple_borrows() {
let x = 42;
// 可以同时存在多个不可变借用
let r1 = &x;
let r2 = &x;
let r3 = &x;
println!("r1: {}, r2: {}, r3: {}", r1, r2, r3);
// 所有借用都有效
println!("x: {}", x);
}
pub fn demonstrate_read_only() {
let x = 42;
let r = &x;
// 不可变借用只能读取
println!("读取: {}", r);
// 不能修改
// *r = 43; // 编译错误!cannot assign to data in a `&` reference
}
pub fn demonstrate_freezing() {
let mut x = 42;
let r = &x;
// 在借用活跃期间,原值被冻结
// x = 43; // 编译错误!cannot assign to `x` because it is borrowed
println!("借用: {}", r);
// r 的最后使用(NLL)
// 借用结束,x 解冻
x = 43;
println!("修改后: {}", x);
}
}
/// 示例 2: 不可变借用与可变借用的互斥
pub mod borrow_exclusion {
pub fn demonstrate_mutual_exclusion() {
let mut x = 42;
let r1 = &x;
// 不能在不可变借用存在时创建可变借用
// let r2 = &mut x; // 编译错误!cannot borrow as mutable
println!("不可变借用: {}", r1);
// r1 的最后使用
// 现在可以创建可变借用
let r2 = &mut x;
*r2 = 43;
println!("可变借用: {}", r2);
}
pub fn demonstrate_reverse_exclusion() {
let mut x = 42;
let r1 = &mut x;
*r1 = 43;
// 不能在可变借用存在时创建不可变借用
// let r2 = &x; // 编译错误!cannot borrow as immutable
println!("可变借用: {}", r1);
// r1 的最后使用
// 现在可以创建不可变借用
let r2 = &x;
println!("不可变借用: {}", r2);
}
}
/// 示例 3: 非词法生命周期(NLL)
pub mod non_lexical_lifetimes {
pub fn demonstrate_nll() {
let mut x = 42;
let r = &x;
println!("借用: {}", r);
// r 的最后使用在这里
// NLL:r 的生命周期在最后使用后结束
// 不需要等到作用域结束
x = 43;
println!("修改: {}", x);
}
pub fn demonstrate_conditional_borrow() {
let mut x = 42;
let condition = true;
if condition {
let r = &x;
println!("条件借用: {}", r);
} // r 的作用域结束
// 可以修改 x
x = 43;
println!("修改: {}", x);
}
pub fn old_style_workaround() {
let mut x = 42;
{
let r = &x;
println!("借用: {}", r);
} // 显式作用域结束借用
x = 43;
println!("修改: {}", x);
}
}
/// 示例 4: 借用分割
pub mod borrow_splitting {
pub struct Point {
pub x: i32,
pub y: i32,
}
pub fn demonstrate_field_borrowing() {
let mut point = Point { x: 10, y: 20 };
// 可以同时借用不同字段
let x_ref = &point.x;
let y_ref = &mut point.y;
println!("x: {}", x_ref);
*y_ref = 30;
println!("y: {}", y_ref);
}
pub fn demonstrate_slice_splitting() {
let mut array = [1, 2, 3, 4, 5];
// 分割借用不同部分
let (left, right) = array.split_at_mut(2);
println!("左: {:?}", left);
println!("右: {:?}", right);
left[0] = 10;
right[0] = 30;
}
}
/// 示例 5: 迭代器与借用
pub mod iterator_borrowing {
pub fn demonstrate_iterator_borrow() {
let vec = vec![1, 2, 3, 4, 5];
// 迭代器持有不可变借用
let iter = vec.iter();
// 在迭代器存在期间不能修改
// vec.push(6); // 编译错误!cannot borrow as mutable
for item in iter {
println!("{}", item);
} // iter 的生命周期结束
// 现在可以修改
// vec.push(6); // 如果 vec 是 mut 的话
}
pub fn demonstrate_iterator_consumption() {
let mut vec = vec![1, 2, 3, 4, 5];
{
let _sum: i32 = vec.iter().sum();
// 迭代器被消费,借用结束
}
// 可以修改
vec.push(6);
println!("{:?}", vec);
}
pub fn demonstrate_collect_pattern() {
let vec = vec![1, 2, 3, 4, 5];
// 需要修改:收集到新集合
let doubled: Vec<i32> = vec.iter()
.map(|&x| x * 2)
.collect();
println!("原始: {:?}", vec);
println!("加倍: {:?}", doubled);
}
}
/// 示例 6: 闭包捕获不可变借用
pub mod closure_borrowing {
pub fn demonstrate_closure_capture() {
let x = 42;
let print_x = || println!("x: {}", x);
// 闭包捕获不可变借用
// 即使不调用,借用也存在
print_x();
print_x(); // 可以多次调用
// x 仍可访问(不可变)
println!("原始 x: {}", x);
}
pub fn demonstrate_closure_lifetime() {
let mut x = 42;
{
let print_x = || println!("x: {}", x);
print_x();
} // 闭包生命周期结束,借用释放
// 现在可以修改
x = 43;
println!("修改后: {}", x);
}
pub fn demonstrate_explicit_drop() {
let mut x = 42;
let print_x = || println!("x: {}", x);
print_x();
// 显式释放闭包
drop(print_x);
// 借用结束,可以修改
x = 43;
println!("修改后: {}", x);
}
}
/// 示例 7: 方法链与借用
pub mod method_chaining {
pub struct Builder {
value: i32,
}
impl Builder {
pub fn new() -> Self {
Self { value: 0 }
}
// 返回 &Self:延长借用
pub fn get_value(&self) -> i32 {
self.value
}
// 返回 Self:消费 self
pub fn with_value(mut self, value: i32) -> Self {
self.value = value;
self
}
}
pub fn demonstrate_chaining() {
let builder = Builder::new()
.with_value(42);
println!("值: {}", builder.get_value());
}
}
/// 示例 8: 内部可变性
pub mod interior_mutability {
use std::cell::{Cell, RefCell};
pub fn demonstrate_cell() {
let x = Cell::new(42);
// Cell 允许通过不可变引用修改
let r = &x;
r.set(43);
println!("Cell 值: {}", x.get());
}
pub fn demonstrate_refcell() {
let x = RefCell::new(vec![1, 2, 3]);
// RefCell 运行时检查借用规则
let r1 = x.borrow(); // 不可变借用
let r2 = x.borrow(); // 可以有多个不可变借用
println!("r1: {:?}", *r1);
println!("r2: {:?}", *r2);
drop(r1);
drop(r2);
// 可变借用
let mut r3 = x.borrow_mut();
r3.push(4);
println!("修改后: {:?}", *r3);
}
pub fn demonstrate_refcell_panic() {
let x = RefCell::new(42);
let _r1 = x.borrow();
// 运行时 panic:违反借用规则
// let _r2 = x.borrow_mut(); // panic: already borrowed
}
}
/// 示例 9: 生命周期参数
pub mod lifetime_parameters {
pub struct Ref<'a> {
data: &'a i32,
}
impl<'a> Ref<'a> {
pub fn new(data: &'a i32) -> Self {
Self { data }
}
pub fn get(&self) -> &i32 {
self.data
}
}
pub fn demonstrate_lifetime() {
let x = 42;
let ref_x = Ref::new(&x);
println!("引用: {}", ref_x.get());
// x 在 ref_x 存在期间被借用
} // ref_x 和 x 一起释放
pub fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
pub fn demonstrate_lifetime_elision() {
let s1 = String::from("hello");
let s2 = String::from("world!");
let result = longest(&s1, &s2);
println!("最长: {}", result);
}
}
/// 示例 10: 常见错误与解决方案
pub mod common_mistakes {
pub fn mistake_modify_while_borrowed() {
let mut vec = vec![1, 2, 3];
let first = &vec[0];
// 错误:在借用存在时修改
// vec.push(4); // 编译错误!
println!("第一个元素: {}", first);
// 解决方案:在使用借用后再修改
vec.push(4);
}
pub fn mistake_return_reference() {
// 错误:返回局部变量的引用
// fn get_reference() -> &i32 {
// let x = 42;
// &x // 编译错误!返回悬垂引用
// }
// 解决方案:返回所有权或使用生命周期参数
fn get_value() -> i32 {
42
}
println!("{}", get_value());
}
pub fn solution_collect_indices() {
let mut vec = vec![1, 2, 3, 4, 5];
// 收集需要删除的索引
let to_remove: Vec<usize> = vec.iter()
.enumerate()
.filter(|(_, &x)| x % 2 == 0)
.map(|(i, _)| i)
.collect();
// 反向删除避免索引偏移
for &i in to_remove.iter().rev() {
vec.remove(i);
}
println!("结果: {:?}", vec);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multiple_immutable_borrows() {
let x = 42;
let r1 = &x;
let r2 = &x;
assert_eq!(*r1, *r2);
}
#[test]
fn test_nll() {
let mut x = 42;
let r = &x;
let _ = *r;
// r 的生命周期结束
x = 43;
assert_eq!(x, 43);
}
#[test]
fn test_borrow_splitting() {
let mut point = borrow_splitting::Point { x: 10, y: 20 };
let x_ref = &point.x;
let y_ref = &mut point.y;
assert_eq!(*x_ref, 10);
*y_ref = 30;
}
}
rust
// examples/immutable_borrow_demo.rs
use code_review_checklist::*;
fn main() {
println!("=== 不可变借用的规则与限制 ===\n");
demo_basic_rules();
demo_nll();
demo_borrow_splitting();
demo_iterators();
demo_closures();
}
fn demo_basic_rules() {
println!("演示 1: 基本不可变借用规则\n");
basic_immutable_borrow::demonstrate_multiple_borrows();
println!();
basic_immutable_borrow::demonstrate_freezing();
println!();
}
fn demo_nll() {
println!("演示 2: 非词法生命周期\n");
non_lexical_lifetimes::demonstrate_nll();
println!();
non_lexical_lifetimes::demonstrate_conditional_borrow();
println!();
}
fn demo_borrow_splitting() {
println!("演示 3: 借用分割\n");
borrow_splitting::demonstrate_field_borrowing();
println!();
}
fn demo_iterators() {
println!("演示 4: 迭代器与借用\n");
iterator_borrowing::demonstrate_iterator_consumption();
println!();
iterator_borrowing::demonstrate_collect_pattern();
println!();
}
fn demo_closures() {
println!("演示 5: 闭包捕获\n");
closure_borrowing::demonstrate_closure_capture();
println!();
closure_borrowing::demonstrate_explicit_drop();
println!();
}
实践中的专业思考
理解借用的活跃期:借用在最后一次使用后结束(NLL),不是作用域结束。
利用借用分割:借用结构体的不同字段互不干扰,提供更灵活的访问。
显式 drop 释放借用 :当需要提前结束借用时,使用 drop(reference) 或显式作用域。
避免长期持有引用:缩短借用的生命周期,让原值尽快可修改。
文档化借用语义:在函数签名和文档中明确说明借用的生命周期。
使用迭代器方法 :retain、drain_filter 等方法避免迭代中修改的问题。
结语
不可变借用是 Rust 借用系统的基础,它通过共享只读访问和严格的编译期检查,实现了既安全又高效的数据访问。从理解不可变借用的核心规则、掌握冻结语义和 NLL、学会借用分割和生命周期管理、到处理迭代器和闭包的限制,不可变借用贯穿 Rust 程序的每个环节。这正是 Rust 的设计哲学------通过类型系统和编译器的精确追踪,在编译期保证共享访问的安全性,让程序员既能享受多读者的并发性能,又不必担心数据竞争和内存安全问题。掌握不可变借用的规则和模式,不仅能写出正确的代码,更能充分利用 Rust 的表达力,构建既安全又优雅的系统。