我必须立刻押注 Rust 之三🎲:你一定要知道的借用与生命周期

324 阅读5分钟

在上文 一文通关所有权(Ownership)系统 中,我们知晓了 引用 是 不获取所有权而访问数据的重要方式。

不获取所有权而访问数据 这一行为就是 借用

借用的基础概念

回顾之前的示例

fn main() {
    let mut s1 = String::from("hello");
    let s2 = &mut s1;
    s2.push_str(", world");
    println!("s2: {}", s2); // s2: hello, world
    println!("s1: {}", s1); // s1: hello, world
}

s1 是数据的所有者。s2 借用了 s1 的数据,也可以说 s1s2 借用。

借用分类

不可变借用 (&T)

可以通过引用读取数据,但不能修改它

fn main() {
    let s = String::from("hello");
    let r1 = &s;  // 不可变借用
    
    r1.push('a'); // error: cannot borrow `s` as mutable, as it is not declared as mutable
    println!("{}", r1);
}

  • 通过 不可变引用(&) 来借用数据
  • sr1 不可变借用 (&T)
  • 不可通过引用 r1 修改数据

可变借用 (&mut T)

可以通过引用读取数据,也可以修改它

fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;  // 不可变借用

    r1.push('a');   // 可变借用
    println!("{}", r1); // helloa
}

  • 通过 可变引用(&mut) 来借用数据
  • sr1 可变借用 (&mut T)
  • 可以通过引用 r1 修改数据

借用规则

Rust 的借用规则是为了确保内存安全并防止数据竞争。主要有以下三条规则:

  1. 在同一作用域内,可以有任意多个不可变引用,或者只能有一个可变引用
  2. 可变引用与不可变引用不能同时存在
  3. 引用必须总是有效,也就是说,不能在引用有效期间使其所指向的数据失效。

多个不可变引用

fn main() {
    let s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    println!("r1: {}, r2: {}", r1, r2);  // 两个不可变引用可以共存
}

单一可变引用

fn main() {
    let mut s = String::from("hello");
    let r1 = &mut s;  
    // let r2 = &mut s;  // 错误!同时只能有一个可变引用
    r1.push_str(", world");
    println!("{}", r1); 
}

不可变引用与可变引用的冲突

fn main() {
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &mut s;  // r1的借用作用域内, 再使用不可变引用, 会发生冲突
    println!("r1: {}", r1);
}

引用必须总是有效

fn main() {
    let x = 5;
    let x2 = x_self(x);
    println!("The value of x2 is: {}", x2);
}

fn x_self (x: i32) -> &i32 { // 错误:返回的引用是悬垂引用
   println!("The value of x is: {}", x);
   return  &x;
}

x_self 函数返回了一个引用,但是引用的数据 x 在函数结束后就会被销毁。因此返回的引用是 悬垂引用 (已经释放或无效内存地址的引用)。

这种行为是不允许的,Rust 编译器会报错。

借用作用域

借用的作用域是指借用的有效范围。

在这个范围内,你不能对原值进行修改,除非借用已经结束。

let mut arr = [1, 2, 3, 4, 5];

let slice = &mut arr[1..3];  // 可变借用 arr 的一部分
// 此时 arr 的部分数据被借用,不能修改其他部分
// arr[0] = 10;  // 错误:arr 仍被部分借用

slice[0] = 20;  // 正确,修改切片内容
println!("{:?}", slice);  // 

// 借用结束后,可以修改 arr
arr[0] = 10;  
println!("{:?}", arr);  // 输出 [10, 20, 3, 4, 5]

可变借用在使用结束后立即释放,作用域结束后原数据便可重新使用。

生命周期(Lifetimes)

值的生命周期

一个值在内存中存在的时间段,也就是它被创建到被销毁的时间范围。

当值的所有者(比如在栈上的局部变量)离开作用域时,值的生命周期就结束了,Rust 会自动释放值所占的内存。

值的生命周期通常和它的作用域一致,而作用域结束时值会被自动丢弃。

引用的生命周期

引用在作用域中有效的时间段。

当你借用一个值时,就创建了一个引用的生命周期。当引用不再被使用时,引用的生命周期就会结束,即使它的值仍然存在。

引用的生命周期必须与它所借用的数据的生命周期匹配。

生命周期示例

fn main() {
    let r;
    {
        let x = 5;
        r = &x;  // 错误:x 的生命周期结束,r 引用了无效数据
    }
    // println!("r: {}", r);  // 错误,x 已经不再存在
}

这也是我们之前示例中

fn x_self (x: i32) -> &i32 { // 错误:返回的引用是悬垂引用
   println!("The value of x is: {}", x);
   return &x;
}

这段函数不会通过编译的原因。因为 x 的生命周期已经结束了,而引用的生命周期必须与它所借用的数据的生命周期匹配。

生命周期标注

Rust 的生命周期机制确保引用永远不会无效。

每个引用都有一个生命周期,它标记了引用的有效范围。

当函数返回引用时,Rust 需要知道引用的生命周期,以确保返回的引用不会在调用者作用域之外失效。

// 'a 是生命周期标注,表示返回的引用的生命周期与传入的引用相同
// Rust 对生命周期标注使用的名称没有要求,只要以 ' 开头。因此,'a、'b、'c 等都是可以的。
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

很多情况下,Rust 不需要你手动添加生命周期标注,编译器会自动处理。但在复杂引用关系中,编译器无法自行推断时,就需要显式标注。

这里 s1 和 s2 都是引用,如果不显示说明,Rust 便无法推断返回值使用谁的生命周期。

其他: 泛型语法

<> 表示泛型参数, 与 TypeScript 中的泛型类似。

可以组合多种参数,例如使用多个生命周期标注和多个泛型参数:

fn complex<'a, 'b, T, U>(s1: &'a str, s2: &'b str, value: T, other: U) {
    // ...
}