月泉的博客

Rust的生命周期及所有权机制

月泉 Rust

简单的一个梳理

Rust在内存管理方面和其它语言不太一样,像C/C++给予程序员无上的权利,可以手动的去管理内存,当然权利越大责任越大同时也就意味着风险越高,目前看来让程序员在运行时对内存进行手工的管理所带来的风险,大多数情况下弊大于利(这仅我个人观点,本人技术有限或许是我眼拙),常会出现忘记释放内存、访问一块已经被释放了的内存等诸如此类内存安全问题,则Java这一类虚拟机高级语言则是采用GC的方式来对内存进行管理,替代程序员去手动管理,这样便是损耗了性能换来内存的自动管理,同时Java也面对着对于数据共享时所会产生的线程安全问题,但Rust另辟蹊径结合以上观点,我觉得也算是从根本上解决了问题,它使用所有权机制来对内存进行管理,这便产生了一些术语:ownership、lifetime。

Rust采用这种机制当然也是会有代价,毕竟如果能够获得可观的回报,那么一定有相应的代价,它的代价可以说,说高不高,说低也不低,我目前所认为的代价有2个

  • 编译速度(这个其实对于我来说不太重要)
  • 学习成本(这些学习成本可能会对于某些基础不好,不善学者的一个劝退

所有权

这里的内容是针对的Move语义的引用类型,并没有涉及到一些Copy语义的值类型,希望大家阅读此文章不要搞混淆,以免发生误导,若尽信书,不如不读书。

对于所有权有几条铁律:

  • 所有的资源有且仅有一个所有者
  • 借用者的生命周期不能长于出借者的生命周期
  • 资源出借后,(可读不可写,可写不可读)或回收内存
    • 不可变出借,可同时被多个借用
    • 可变借用,只能出借个一个人,同时也不可出借不可变借用,独享
  • 当所有者超出域就会被回收

用代码来解释这几条铁律

所有的资源有且仅有一个所有者

fn main() {
    let hello = String::from("Hello");
    println!("hello = {:?}", hello);
    println!("hello's memory address = {:p}", hello.as_ptr());
    let hello2 = hello; // 所有权发生变化,move给了hello2
    println!("hello2 = {:?}", hello2);
    println!("hello2's memory address = {:p}", hello2.as_ptr());
}

从上述代码可以看出来hello2hello指针指向的内存地址是同一块,但上上述代码并未体现出所有的资源有且仅有一个所有者的含义,接着再添加一行代码

fn main() {
    .....
    println!("{:?}", hello); //  value borrowed here after move
}

编译器即刻报错,所有权已经move给了hello2所以不可以再访问hello

借用者的生命周期不能长于出借者的生命周期

如果借用者的生命周期长于出借者的生命周期,那么所产生的问题最直观的问题就是悬垂指针即访问一块已经被回收了的内存

fn main() {
    let x;
    {
        let apple = String::from("Red Apple");
        x = &apple; // borrowed value does not live long enough
    }
    println!("{:?}", x);
    
}

x是所有权的借用者,从上代码可以直观的看到apple是所有权的出借者,它所定义的域要比x的生命周期要短

资源出借后,(可读不可写,可写不可读)或回收内存

可读不可写

fn main() {
    let mut apples = vec!();
    apples.push("Red Apple");
    let gift_box = &apples;
    apples.push("Yellow Apple"); // mutable borrow occurs here
    println!("{:?}", apples);
    println!("{:?}", gift_box);
}

从上述代码可以直观的看出来,当资源被不可变出借,且所有权未归还之前,所有者不可对其在进行写操作,同时也不可可变出借,见如下代码:

fn main() {
    let mut apples = vec!();
    apples.push("Red Apple");
    let gift_box = &apples;
    let gift_box2 = &mut apples; // mutable borrow occurs here
    println!("{:?}", apples);
    println!("{:?}", gift_box);
}

不可变借用可以同时出借多次,见如下代码:

fn main() {
    let mut apples = vec!();
    apples.push("Red Apple");
    let gift_box = &apples;
    let gift_box2 = &apples;
    let gift_box3 = &apples;
    println!("{:?}", apples);
    println!("{:?}", gift_box);
    println!("{:?}", gift_box2);
    println!("{:?}", gift_box3);
}

可写不可读

并不是说不可读,其意是说,出借可变借用后在未归还之前不可再出借可读借用

fn main() {
    let mut apples = vec!();
    apples.push("Red Apple");
    let mut apple_pool = &mut apples;
    let gift = &apples; // ^^^^^^^ immutable borrow occurs here
    println!("{:?}", apple_pool);
}

同样也不可出借多个可变借用

fn main() {
    let mut apples = vec!();
    apples.push("Red Apple");
    let mut apple_pool = &mut apples;
    let gift = &mut apples; //  ^^^^^^^^^^^ second mutable borrow occurs here
    println!("{:?}", apple_pool);
}

同理所有者也不可对它进行修改

fn main() {
    let mut apples = vec!();
    apples.push("Red Apple");
    let mut apple_pool = &mut apples;
    apples.push("Yellow Apple"); //  ^^^^^^ second mutable borrow occurs here
    println!("{:?}", apple_pool);
}

小结

其非常像读写锁的概念,只是只要清楚,共享不可写,可写不共享就很容易理解这个概念

当所有者超出域就会被回收

代码如下,这里也不想做太多无畏的解释,代码见真章?

fn main() {
    { // 为了特意强调,加了一个块作用域
        let a = String::from("A");
    }
}

a离开它的域后,该所有者及其内存就会被回收,我该如何证明?很简单,看下其生成的MIR代码就可以了。

以下MIR代码经过我删减

fn main() -> (){
    
    scope 2 {
        let _1: std::string::String;     // let a;
    }

    bb0: {                              
        StorageLive(_1);
        _1 = const std::convert::From::from(const "A") -> bb1; // a = String::from("A");
        
    bb1: {                              
        drop(_1) -> bb2;                 // bb1[0]: scope 0 at src/main.rs:4:5: 4:6 第四行 就已经drop掉了也就是}后
    }
    bb2: {                              
        StorageDead(_1);                 // bb2[0]: scope 0 at src/main.rs:4:5: 4:6
        return;                          // bb2[1]: scope 0 at src/main.rs:5:2: 5:2
    }
}

生命周期

Rust的生命周期和其它语言不同,通常我们也称它为Lifetime,它用于辅助Rust在编译期间就能够确定一个对象生命周期,而不用等到运行时才能发现问题,所以也不会产生任何运行时开销,同时对如何静态分析一个对象什么时候回收,就和上述我所描述的所有权机制的铁律相关,因为静态分析也是遵循它来的

用代码最直观的感受下Rust的生命周期

struct Node {
    name: String
}

impl Drop for Node {
    fn drop(&mut self) {
        println!("{}", self.name);
    }
}

fn main() {
    let a = Node{ name: String::from("A") };
    let b = Node{ name: String::from("B") };
    let c = Node{ name: String::from("C") };
    let d = Node{ name: String::from("D") };
}

从上述代码的析构函数执行顺序,可见输出为:

D
C
B
A

此段代码中其生命周期如果要表示出来就像这样

'a {
    let a = Node{ name: String::from("A") };
    'b{
        let b = Node{ name: String::from("B") };
        'c{
            let c = Node{ name: String::from("C") };
            'd{
                let d = Node{ name: String::from("D") };
            }
        }
    }
}

虽然编译器很强大,能够帮住我们在编译阶段就完成对内存的管理,但是有的时候编译器还是准确的分析出我们的对象什么时候回收,所以需要手工标注“生命周期标识”,但请记住它仅是一个标识只是用于标识生命周期辅助编译器分析,没有决定性的含义,需要手动标识生命周期大多都是出现在引用借出的时候却又和输出参数挂钩时。

函数中的生命周期标识

见如下代码:

fn get_a_number(a: &i32, b: &i32) -> &i32 { // ^ expected lifetime parameter
    if true { a } else { b } 
}
fn main(){
    let a = 10;
    let b = 20;
    let c = get_a_number(&a, &b);
    println!("{:?}", c);
}

上述代码就无法通过编译因为编译器无法确定,返回的是a还是b,以及它们的生命周期长短,如果还没有理解没有关系,我再将以上代码调整一下

fn get_a_number(a: &i32, b: &i32) -> &i32 { // ^ expected lifetime parameter
    if false { a } else { b } 
}
fn main(){
    let a = 10;
    let c;
    {
        let b = 20;
        c = get_a_number(&a, &b);
    }
    println!("{:?}", c);
}

如果这样的情况下,代码通过了编译返回了b的借用给c,最终离开作用于b所有权是会被释放掉的吧?但其又还有外部的引用,还记得那一句“借用方的生命周期不可长于出借方”的铁律吗?如果这样上述通过编译最直观体现的问题就是悬垂指针,接着给它加上生命周期标识

fn get_a_number<'a>(a: &'a i32, b: &'a i32) -> &'a i32 {
    if false { a } else { b } 
}
fn main(){
    let a = 10;
    let c;
    {
        let b = 20;
        c = get_a_number(&a, &b); // ^^ borrowed value does not live long enough
    }
    println!("{:?}", c);
}

会发现Rust编译器就能够推断这种情况,接着为了能够通过编译,我再将代码修改为:

fn get_a_number<'a>(a: &'a i32, b: &'a i32) -> &'a i32 {
    if false { a } else { b } 
}
fn main(){
    let a = 10;
    let b = 20;
    let c = get_a_number(&a, &b);
    println!("{:?}", c);
}

'a的意义实际上只是起到标识性的作用,甚至可以命名为'this_is_long_name,它标识在ab以及出参上的用意是提醒编译器,它们的生命周期是相等的,当然同时还可以有多个标识,我继续将上述代码修改:

fn get_a_number<'a, 'b>(a: &'a i32, b: &'b i32) -> &'a i32 { 
    if false { a } else { b } 
}
fn main(){
    let a = 10;
    let b = 20;
    let c = get_a_number(&a, &b);
    println!("{:?}", c);
}

接着就会发现编译器报错this parameter and the return type are declared with different lifetimes,因为它发现有参数的返回的生命周期和输出参数的生命周期是不一样的,所以编译器友好的提示我们需要再进一步的修改代码,但实际上如果入参是和输出参数是无关的倒无关紧要,如下:

fn get_a_number<'a, 'b>(a: &'a i32, b: &'b i32) -> &'a i32 {
    if false { a } else { a } 
}
fn main(){
    let a = 10;
    let b = 20;
    let c = get_a_number(&a, &b);
    println!("{:?}", c);
}

但有的时候就需要标识多个生命周期标识该怎么做呢?很简单推断出合法的生命周期,如何判定是否合法?很简单一句话搞定,输出值的Lifetime是输入参数的所有的Lifetime的交集即为合法的生命周期,所以我们可以将代码修改如下:

fn get_a_number<'a, 'b: 'a>(a: &'a i32, b: &'b i32) -> &'a i32 {
    if false { a } else { b } 
}
fn main(){
    let a = 10;
    let b = 20;
    let c = get_a_number(&a, &b);
    println!("{:?}", c);
}

'b声明中包含生命周期'a使生命周期'a为生命周期'b的子集,表示'b的生命周期比'a的生命周期要长

当然其实生命周期有的时候是可以省略的,有一定的省略规则,先看如下代码

fn get_a_string(a: &str) -> &str{
    a
}

fn main(){
    let a = String::from("A");
    println!("{:?}", a);
}

实际上等同于

fn get_a_string<'a>(a: &'a str) -> &'a str{
    a
}

只不过编译器允许我们省略了,省略的规则如下:

  • 每一个是引用的参数都有它自己的生命周期参数
  • 如果只有一个输入生命周期参数,那么它就是所有输出生命周期参数
  • 如果方法有多个输入生命周期参数,不过其中之一因为方法的缘故为 &self&mut self,那么 self 的生命周期被赋给所有输出生命周期参数。

第一条规则其实还挺好理解的,就比如:

fn get_a_string<'a, 'b>(a: &'a str, b: &'a str) -> &'a str{
    ...
}

第二条规则,不多解释字面意思看码

fn get_a_string<'a>(a: &'a str) -> &str{
    a
}

第三条规同理也是字面意思

fn xxxx(&self) -> i32 {
     .....
}

小结

这篇文章我觉得是言简意赅的说明了对所有权机制和rust的生命周期,希望对大家理解Rust的所有权机制及生命周期有帮助,反正不多逼逼“Rust 天下第一!!!”

引用

Rust编程之道》东哥的新书,我强烈推荐,在我心中这本书等同于《Thinking in Java》,此书在各大商城以及亚马逊Kindle商店均有出售

https://doc.rust-lang.org/book/ch19-02-advanced-lifetimes.html

https://doc.rust-lang.org/book/ch10-03-lifetime-syntax.html#generic-lifetimes-in-functions

月泉
伪文艺中二青年,热爱技术,热爱生活。