在Rust中,如何正确地使用不可变借用和可变借用

在Rust中,借用是访问数据的一种方式,可以是不可变的或可变的。正确使用不可变借用和可变借用对于避免编译错误和确保内存安全至关重要。以下是一些关键点和规则:

在Rust中,借用是访问数据的一种方式,可以是不可变的或可变的。正确使用不可变借用和可变借用对于避免编译错误和确保内存安全至关重要。以下是一些关键点和规则:

不可变借用(Immutable Borrowing)

  • 定义:不可变借用允许你读取数据,但不允许修改。
  • 规则:
    • 同一时间内,你可以拥有多个不可变借用。
    • 不可变借用不能与可变借用同时存在。

可变借用(Mutable Borrowing)

  • 定义:可变借用允许你修改数据。
  • 规则:
    • 同一时间内,只能有一个可变借用。
    • 可变借用不能与另一个可变借用或任何不可变借用同时存在。

使用场景

  • 当你需要修改数据时,使用可变借用。
  • 当你只需要读取数据,不希望或不需要修改数据时,使用不可变借用。

语法示例

fn main() {
    let mut x = 5; // x 是可变的

    // 可变借用
    {
        let y = &mut x; // 创建一个可变引用 y
        *y += 1; // 修改 x 的值
    } // y 在这里超出作用域

    // 不可变借用
    {
        let z = &x; // 创建一个不可变引用 z
        println!("{}", z); // 打印 x 的值
    } // z 在这里超出作用域

    // 多个不可变借用
    {
        let a = &x;
        let b = &x;
        // 可以同时存在多个对 x 的不可变引用
    } // a 和 b 在这里超出作用域

    // 错误示例:尝试在存在不可变借用时创建可变借用
    // let c = &x; // 不可变借用
    // let d = &mut x; // 错误:不能同时存在可变借用和不可变借用
}

注意事项

  • 作用域:借用必须在它们的作用域内有效。一旦作用域结束,借用也会结束。
  • 数据竞争:Rust的借用规则帮助避免数据竞争,即多个线程同时以可变方式访问同一数据。
  • 编译时检查:Rust编译器会在编译时检查借用规则,违反规则的代码将无法编译。

实际开发中的常见模式

RefCell:内部可变性

当你需要在不可变引用内部修改数据时,可使用 RefCell 实现运行时检查的内部可变性:

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(vec![1, 2, 3]);
    
    // 不可变借用 data,但内部可以修改
    {
        let mut borrow = data.borrow_mut();
        borrow.push(4);
    } // borrow_mut 在此处释放
    
    println!("{:?}", data.borrow()); // [1, 2, 3, 4]
}

⚠️ 注意:RefCell 在运行时检查借用规则,违反时会 panic,因此仅适用于单线程场景。

借用与生命周期

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let s1 = String::from("hello");
    let result;
    {
        let s2 = String::from("world");
        result = longest(&s1, &s2);  // s2 的生命周期必须 >= result
    } // s2 在此处 drop
    // println!("{}", result); // 编译错误:result 引用了已释放的 s2
}

迭代器中的借用模式

fn main() {
    let mut items = vec![1, 2, 3, 4, 5];
    
    // 不可变遍历
    for item in &items {
        println!("{}", item);
    }
    
    // 可变遍历(同时修改)
    for item in &mut items {
        *item *= 2;
    }
    
    // 使用后再次不可变遍历:✅ 合法
    for item in &items {
        println!("{}", item); // 2, 4, 6, 8, 10
    }
}

借用规则速查表

场景是否允许说明
多个不可变借用✅任意数量,同时存在
一个可变借用✅独占访问权
不可变 + 可变借用同时存在❌会导致数据竞争
多个可变借用同时存在❌违反唯一性原则
借用后移动所有权❌借用期间所有权被借用
同一作用域内交替借用✅NLL(Non-Lexical Lifetimes)允许

NLL(非词法生命周期):Rust 2018+ 允许借用只在实际使用期间有效,而非整个作用域。

为什么编译器要强制这些规则?

编译器强制借用规则的根本目的是在编译期消灭数据竞争:

数据竞争三要素:
1. 两个或多个指针同时访问同一数据
2. 至少有一个指针用于写入
3. 没有同步机制

Rust 借用规则直接禁止 1+2 同时发生:
- 多个读取者 → ✅ 安全(无写入)
- 单个写入者 → ✅ 安全(无其他访问者)
- 读写同时 → ❌ 编译错误

这与 Go/Java 的读写锁(RWMutex/ReadWriteLock)理念一致,但 Rust 在编译期就完成检查,零运行时开销。

FAQ

Q1: &T 和 &mut T 可以相互转换吗?
不可以直接转换,但可以通过重新借用实现:

let mut x = 5;
let r1 = &mut x;
let r2 = &*r1;  // 将 &mut i32 重新借用为 &i32

Q2: String 和 &str 的关系是什么?
String 拥有堆内存的所有权,&str 是对字符串切片的不可变借用。函数参数优先使用 &str,更灵活且零拷贝。

Q3: 为什么 Vec::push 需要 &mut self?
因为 push 可能触发重新分配(扩容),这会改变内部指针,若同时有给其他元素的引用就会悬垂指针。

通过遵循这些规则,你可以在 Rust 中安全地使用不可变和可变借用,确保你的程序既安全又高效。编译器的严格检查虽然初期学习曲线较陡,但一旦掌握,将从根本上杜绝内存安全和数据竞争问题。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「rust」更多文章

  1. 性能剖析与优化
  2. 测试与基准:criterion 与 proptest
  3. Serde 与序列化生态