Rust 生命周期管理

2023-06-15 rust language

所有权 Ownership 是 Rust 的核心功能之一,使得 Rust 无需垃圾回收,却仍然可以保证内存安全。

所有权

Rust 主要解决的是两个问题:A) 如何安全地进行系统编程;B) 如何更容易并发。而 Rust 通过 Ownership 同时解决了这两个问题,也就是,让 Rust 安全的同时可以直接解决并发问题。

所有权的规则为:

  • 每个值都有一个被称为其所有者的变量。
  • 值在任一时刻有且只有一个所有者。
  • 当所有者离开作用域时,这个值将被丢弃。

在 Rust 中,字符串字面值硬编码在可执行文件中,不可变化,不过可以通过 String 类型,该类型会分配到堆上,能够动态处理变长的字符串,可以通过 let mut s = String::from("Hello") 进行定义。

默认会使用移动操作,对于像 String 这种在堆上分配的内存,在移动之后原值将失去所有权,也就无法再使用;当然,也可以通过 clone() 函数完全复制一份。

fn main() {
    let s1 = String::from("Hello World");
    let s2 = s1;
    let s3 = s2.clone();

    println!("{}", s2); // s1的所有权已经转移,无法直接打印s1变量
    println!("{}", s3); // s3是复制了一份,所以s2和s3都可以使用
}

对于一些基础的变量类型,实际上已经实现了 Copy Trait,那么其通过赋值移动实际上是不影响原变量的,包括了整型、浮点、布尔、字符以及包含这些类型的元组。

fn main() {
    let s1 = 10;
    let s2 = s1;

    println!("{} {}", s1, s2); // 整型实现了Copy Trait,赋值时没有发生所有权的转移
}

所有权转移

赋值、函数入参、函数返回等都会发生所有权的转移。

fn say_hi(s: String) -> String {
    println!("Hi {}!", s);
    s
}

fn main() {
    let s1 = String::from("Andy");
    let s2 = say_hi(s1);
    //println!("{}", s1); // 所有权已转移会报错
    println!("{}", s2); // 所有权通过返回值返回
}

有时候仅仅是使用某个变量,而不是将所有权进行转移,那么此时就可以使用引用,类似于其它语言中的指针。

fn main() {
    let x = String::from("Hello");
    let x1 = &x;
    let x2 = x1; // 引用有Copy Trait可以直接赋值
    println!("{} {} {}", x, x1, x2);

    let mut site = String::from("HelloWorld");
    let domain = &mut site; // 可以修改,引用对象也必须是mut才可以
    domain.push_str(".com");
    println!("{}", domain);
}

生命周期

与大多数语言类似,每个函数都会有一个作用域,也可以在函数内使用一对花括号来内嵌一个作用域,在作用域中声明的变量会在作用域结束时结束。再结合到借用的概念,就会约束借用方的生命周期不能比出借方还要长。

fn main() {
    let a;             // -------------+-- a start
    {                  //              |
        let b = 5;     // -+-- b start |
        a = &b;        //  |           |
    }                  // -+-- b over  |
    println!("{}", a); //              |
}                      // -------------+-- a over

其中 a 是借用方而 b 是出借方,此时借用方的生命周期要比出借方长,会导致悬垂引用。

函数

比较容易出问题的是再结合函数时,其入参是借用方,而返回参数是出借方,例如如下的示例。

fn longer(x: &String, y: &String) -> &String {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let x = String::from("Hi");         // -------------+-- x start
    let max;                            // -------------+-- max start
    {                                   //              |
        let y = String::from("Hello");  // -------------+-- y start
        max = longer(x, y);             //              |
    }                                   // -------------+-- y over
    println!("max {}", max);            //              |
}                                       // -------------+-- max, x over

因为不知道 longer() 函数返回值的声明周期是什么,有可能是新建的,也有可能是与入参相同,所以,如果直接编译会报 error[E0106]: missing lifetime specifier 错误,

此时需要通过函数后的 <> 来指明生命周期,后面的参数以及返回值需要通过 ' 引用即可。

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

fn main() {
    let x = String::from("Hi");
    let max;
    {
        let y = String::from("Hello");
        max = longer(&x, &y);
    }
    println!("max {}", max);
}

上述意味着入参 a b 的生命周期和返回值相同,但是入参的生命周期不同,此时会选择较小的一个,所以,上述编译会报 error[E0597]: y does not live long enough 的错误。

如果有多个生命周期参数,那么就需要标注各个参数之间的关系。

fn longer<'a, 'b: 'a>(x: &'a String, y: &'b String) -> &'a String {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() { 
    let x = String::from("Hi");
    let y = String::from("Hello");
    let max = longer(&x, &y);
    println!("max {}", max);
}

如上通过 'b: 'a 标识 'a 的生命周期不能超过 'b

结构体

如果结构体中包含引用成员,那么就必须保证结构体本身的生命周期不能超过任何一个引用成员的生命周期,否则,会出现成员销毁后结构体仍然会引用成员,同样造成悬垂引用。

#[derive(Debug)]
struct Foo<'a> {
    v: &'a i32
}

fn main() {
    let foo;                    // -------------+-- foo start
    {                           //              |
        let v = 123;            // -------------+-- v start
        foo = Foo {             //              |
            v: &v               //              |
        }                       //              |
    }                           // -------------+-- v over
    println!("foo: {:?}", foo); //              |
}                               // -------------+-- foo over

结构体变量 foo 的生命周期还没有结束,但是其对应的成员变量引用已经结束,所以会报错。

Static

这是一个特殊的生命周期,与整个应用相同,存储在静态段中,可以通过类似 let s: &'static str = "Hello" 这种方式声明,因为所有的字符串字面值实际都是静态的,所以可以简写为 let s: &str = "Hello" 。 另外,通过 static 所声明变量的生命周期也是 'static 的,例如 static v: i32 = 123

参考