Rust引用与生命周期

19 次浏览
2024年07月15日创建

拥有型指针与非拥有型指针(即引用)

let s = "good".to_string();
let v = vec![32, 23];

引用如:

let y = &s;

但:引用的生命周期不会超过其引用的目标。Rust把创建对某个值的引用的操作称为借用。

共享引用和可变引用

共享引用允许读取,但不能修改其引用目标。

let s = "good".to_string();
let refs = &s;
refs = 3;//报错

如果是可变引用

let mut x = "not good".to_string();
let refx = &mut x;
*refx = "very good".to_string();
print!("x {}", x);

let o = 10;
let p = 20;
let mut r = &o;
let b = true;
if b{
  r = &p;
}
println!("{}", *r);

一定要声明mut才行。

Rust是通过*运算符显式进行解引用。但.运算符会按照需求进行左操作数隐式解引用:

struct Anime { name: &'static str, bechdel_pass: bool }
let aria = Anime { name: "Aria: The Animation", bechdel_pass: true};
let anime_ref = &aria;
assert_eq!(anime_ref.name, "Aria: The Animation");
// 与上一句等效,但把解引用过程显式地写了出来 assert_eq!((*anime_ref).name, "Aria: The Animation");

例如方法调用:

let mut v = vec![1973, 1968];
v.sort();
(&mut v).sort();

引用的引用

struct Point { x: i32, y: i32 }
let point = Point { x: 1000, y: 729 };
let r: &Point = &point;
let rr: &&Point = &r;
let rrr: &&&Point = &rr;

对任意表达式结果值的引用

fn factorials(n: u32) -> u32 {
    (1..=n).product()
}

fn main() {
    let r = &factorials(6); // r 是一个指向 u32 类型的引用
    // 数学运算符可以“看穿”一层引用
    assert_eq!(r + &1009, 1729); // 这里使用了引用的加法
}

当执行 r + &1009 时,Rust 会自动对 r 和 &1009 进行解引用,并计算它们所指向值的和。这里,factorials(6) 返回的是 720(6! 的结果),所以 720 + 1009 等于 1729。

这种隐式解引用的行为是通过 std::ops::Add trait 和相关的 Add trait 实现来实现的,它允许你对引用进行数学运算,就好像它们是实际的值一样。这使得函数可以接受引用作为参数,同时不必在每次运算时显式解引用。

这段代码的意图是,通过引用来进行加法运算,并断言结果是否正确。如果结果是 1729,则断言通过,否则程序会在运行时 panic。

引用最关键的部分:生命周期

Rust想要保证不会产生悬垂指针,所以一定会在设计上下功夫,来避免悬垂指针的产生,因为引用不会超过其目标对象的生命,所以这部分归根到底是研究对象的生命周期。而Rust的编译对生命周期的严格检查,其实就是避免上线之后可能出现错误,所以对程序员编写代码产生了一大堆的约束,同时也对程序员有更高的要求。

这也就是为什么:Rust的代码编译通过,上线后基本不会有什么错误的原因。

引用生命周期简单释例

引用的对象已经过了生命周期:

#[test]
fn rc_test(){
    let r;
    {
        let y = 10;
        r = &y;
    }
    print!("r {}", *r);
}

上面y都随着block结束,直接没了,对它的引用自然就非法了。这个流程里,rust在编译阶段就能帮你分析出来,牛逼啊。

引用作为函数参数

如果将引用作为函数参数呢?编译器需要知道,这个函数的返回值,或者这个参数会产生什么影响。在Rust中,函数的签名总会揭示出函数体的行为。

fn f(p: &'static i32) {...}

let x = 10;
f(&x);

因为x不是static,所以无法编译:引用&x的生命周期不能超出x,但通过将它传给f,又限制了它必须至少和'static一样长。没办法做到两全其美,所以Rust只好拒绝了这段代码。

fn f(p: &'a i32) {...}

let x = 10;
f(&x);

上面代码就没问题了,如果一个函数调用通过了编译器,说明生命周期就不存在问题了。

引用作为函数返回值

#[test]
fn rc_test(){
    let s; 
    {
        let parabola = [9, 4, 1, 0, 1, 4, 9];
        s = smallest(&parabola);
    }
    assert_eq!(*s, 0); // 错误:指向了已被丢弃的数组的元素
}
fn smallest(v:&[i32]) -> &i32{
    let mut s = &v[0];
    for r in &v[1..] {
        if *r < *s {
            s = r;
        }
    }
    s
}

上面的代码中,函数smallest的方法签名其实是:

fn smallest<'a>(v: &'a [i32]) -> &'a i32 

表明参数和返回值具有相同的生命周期,而当我们assert_eq的时候,就发生问题,因为s的生命周期应该和parabola一致的,但是parabola已经结束,故报错,你想的没错,如果是这样,就不会报错了:

#[test]
fn rc_test(){
    let parabola = [9, 4, 1, 0, 1, 4, 9];
    let s;
    {
        s = smallest(&parabola);
    }
    assert_eq!(*s, 0); // 错误:指向了已被丢弃的数组的元素
}

当我们想要自己的程序更加健壮时,例如:

struct Ss<'a> {
    r : &'a i32
}

程序员之所以这么声明的原因是:让编译器去帮助程序员发现潜在的生命周期问题。因为程序员大概率知道,我声明Ss的目的是什么,用在什么地方,我的参数r应该是什么。

假设没有生命周期:

// 这无法编译 
struct S {
  r: &i32 
}

let s; 
{
  let x = 10;
  s = S { r: &x }; 
}
assert_eq!(*s.r, 10); // 错误:从已被丢弃的`x`中读取

上面的代码,不经过检查直接上线,那么极有可能发生灾难性的后果。

结构体中相同生命周期可能引发的问题

我们避免不了定义复杂的结构体,为了适应更多场景,我感觉可能需要频繁修改生命周期啊?

struct S<'a> {
  x: &'a i32,
  y: &'a i32 
}

而下面的代码:

let x = 10;
let r;
{
  let y = 20; 
  {
        let s = S { x: &x, y: &y };
        r = s.x; 
  }
}
println!("{}", r);

就会发生报错,s的两个参数说明'a的生命周期是x和y的交集,即不能长于&y,但是r=s.x;就要求了'a的生命周期=r,最终情况下没有一个这样的交集既< &y的'a又>=r的'a的生命周期。这一切检查都交给编译器去发现,但程序员想要解决这个问题,就必须和编译器一样思考,否则找不到错误怎么解决啊。

不同生命周期

struct S<'a, 'b> {
    x: &'a i32,
    y: &'b i32 
}

使用方式还是没有变,发现可以通过编译了。这样做的缺点是,添加生命周期会让类型和函数签名更难阅读。我们 倾向于先尝试尽可能简单的定义,然后放宽限制,直到代码能编译通过为止。由于 Rust 不允许不安全的代码运行,因此简单地等到报告问题时再修改也是一种完全可以接受的策略。

不过我们仍然需要了解,对函数入参数或者某个类型进行引用操作,这个引用可以分为只读和可变两种,会对结果产生什么结果?

一般类型倒是很好理解:

#[test]
fn rc_test_1(){
    let mut x = 10;
    let r1 = &x;
    let r2 = &x;

    x += 10; //r1 是引用x,但是x被借出了,所以肯定是报错的,不能赋值
    let m = &mut x; //不能把x借入为可变引用,因为它涵盖在已借出的不可变引用的生命周期内
    println!("{} {} {}", r1, r2, x);

    let mut y = 20;
    let m1 = &mut y;
    let m2 = &mut y; //不能两次借入可变引用,什么时候是借入,什么时候是借出
    let z = y; //不能使用y,因为在已借出的可变引用的生命周期内
    println!("{}, {}, {}", m1, m2, z); // 在这里使用这些引用
}

特喵的,怎么这么乱啊。我擦。

在 Rust 中,共享引用(不可变引用)和可变引用的使用场景可以简单概括如下:

共享引用 (&T)

只读访问:当你需要读取数据但不需要修改它时,你应该使用共享引用。

多处访问:多个共享引用可以同时存在,允许数据被多个地方同时访问读取。

函数参数:如果一个函数只需要读取参数而不修改它,应该接受共享引用。

线程安全:共享引用本身是线程安全的,可以安全地在多个线程间传递,只要它们都只是读取数据。

可变引用 (&mut T)

写入或修改:当你需要修改数据时,你应该使用可变引用。

独占访问:在任何给定时刻,只能有一个可变引用存在,这防止了数据竞争和条件竞争。

函数参数:如果一个函数需要改变参数的值,它应该接受一个可变引用。

局部变化:在某个作用域中对数据进行修改时,使用可变引用可以避免取得数据的所有权。