Rust 生命周期

生命周期时一种泛型。普通泛型可以确保类型拥有期望的行为,生命周期能够确保引用在我们的使用过程中一直有效。

Rust的每个引用都有自己的生命周期,它对应着引用保持有效性的作用域。 在大多时候,生命周期都是隐式的且可以被推导出来的,就如同大部分时候类型也是可以被推导的一样。当出现了多种可能的类型时,我们就必须手动的声明类型。类似的,当引用的声明周期可能以不同的方式相互关联时,我们就需要手动标注生命周期,确保引用有效。

我的总结:当可能作为返回值的传入参数有多个引用,且没有使用self,且有多个引用与返回值有关,需要使用生命周期标注。

使用生命周期来避免悬垂引用

来看一份错误代码:

//错误代码
fn main() {
    {
        let r;
        {
            let x = 5;
            r = &x;
        }
        println!("r: {}", r);
    }
}

明显报错了,引用r所指向的x的值已经在上面的作用域中销毁了。

Rust时如何去顶这段代码不合法的呢?我们来了解一下借用检查器。

借用检查器

//错误代码
fn main() {
    {
        let r;                // ---------+-- 'a
                              //          |
        {                     //          |
            let x = 5;        // -+-- 'b  |
            r = &x;           //  |       |
        }                     // -+       |
                              //          |
        println!("r: {}", r); //          |
    }                         // ---------+
}

r的生命周期标注为’a,x的生命周期标注为’b,如果被引用者的生命周期短于引用者,则不可引用。 (我觉得这里的表述有点奇怪)

正确的代码:

fn main() {
    {
        let x = 5;            // ----------+-- 'b
                              //           |
        let r = &x;           // --+-- 'a  |
                              //   |       |
        println!("r: {}", r); //   |       |
                              // --+       |
    }                         // ----------+
}

函数中的泛型生命周期

我们先写一个函数,比较两个字符串切片长度,返回最长的字符串切片。并且不获得所有权。 于是我们写了下面的这个错误代码:

//错误代码
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

错误的原因是:longest可能返回x或y的引用,但是rust并不能确定最后是谁返回,因为借用检查器不知道x与y的生命周期是如何与返回值的生命周期相关联的,if语句块下面返回了x,else语句块下面返回了y,我们也不知道谁会被执行。所以我们需要添加一个生命周期泛型,使得借用检查器可以正常运行。

生命周期标注语法

在不影响生命周期的前提下,标注本身会被用于描述多个引用生命周期之间的关系。

可以这样声明生命周期标注:

&i32        // a reference
&'a i32     // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime

解释一下最后一个:拥有生命周期’a的指向i32的可变引用。

单个生命周期标注本身并没有多大意义,因为生命周期标注之所以存在就是为了向rust描述多个泛型生命周期参数之间的关系。比如参数first和参数second都有生命周期a’,那么first和second的引用必须和这里的泛型生命周期存活一样长的时间。

函数签名中的生命周期标注(重点,官方文档难理解)

接下来我们来为longest函数添加生命周期标注:

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

因为生命周期也是泛型所以记得要在longest后面加<'a>。代码的函数签名向rust表明,函数所获取的两个参数的存活时间,必须不短于’a,且返回值也可以获取不短于a’的生命周期。

说了半天,那么我们定义的a’的生命周期到底是多长呢? 被与用于替代’a的具体生命周期就是x的作用域和y的作用域重叠的那一部分,来看一份错误代码:

//错误代码
fn main() {
    let string1  = String::from("xxxxx");                     //-------------+---'x
    let result;                                               //             |
    {                                                         //             |
        let string2  = "yyy".to_string();                     //---+'y 'a    |
        result = longest(string1 .as_str(), string2.as_str());//   |         |
        println!("The longest string is {}", result);         //   |         |
    }                                                         //---+         |
    println!("{}",result);                                    //             |
}                                                             //-------------+

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

返回值的result的生命周期原本是类似’x,调用了longest后result的生命周期并没有改变,但是使用result的合法范围发生了改变,因为借用检查器判断可能出现非法引用(longest返回的是string2的引用就非法了),所以println!("{}",result); 将会报错。

我的理解:声明周期标注在函数调用后通过参数,才会确定具体生命周期,生命周期是同标注的传入参数的交集,明确了生命周期后,传入给借用检查器。至始至终,并没有哪个变量的生命周期被改变,但是合法使用的周期却可能改变了。 我引入了一个合法周期的概念,区别于生命周期,是指不被借用检查器拒绝编译的生命周期。

深入理解生命周期

指定生命周期的方式往往取决于函数的具体功能。

来看一份代码:

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

因为返回值与y完全没有关系,所以可以不指定y的生命周期。

再看一份错误代码:

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}

x和y都没有定义生命周期,所以’a无法确定生命周期,返回值也无法确定生命周期。 即使我们正确定义了生命周期标注,result也无法使用,因为,result的生命周期只到这个函数的尽头。

结构体定义中的生命周期标注

结构体中可以添加引用,指向其他变量。不过需要为结构体定义中的每一个引用都添加生命周期标签。

语法如下:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

可以尝试分析i关于’a的生命周期。

生命周期省略

在没有显式标注生命周期的情况下,编译器目前使用了三种规则来计算引用的生命周期。这些规则不但对fn生效,也对impl生效。第一条用于输入,二三两条用于输出。

第一条规则:每一个引用参数都会拥有自己的生命周期参数。

fn foo<'a, 'b>(x: &'a i32, y: &'b i32);

第二条规则:当只存在一个输入生命周期参数时,这个生命周期会被赋予给所有输出生命周期参数。

fn foo<'a>(x: &'a i32) -> &'a i32

第三条规则: 当拥有多个输入生命周期参数,而其中一个时&self或&mut self时,self的生命周期会被赋予给所有的输出生命周期参数。

方法定义中的生命周期标注

因为生命周期省略第三规则的存在,大部分时候写方法是可以不关注生命周期的。 但是生命在impl以及类型名称后面的生命周期标注是不能省略的。

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}
struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

静态生命周期

rust中还存在一种特殊的生命周期:'static 表示整个程序的执行期,所有的字符串字面量都拥有静态生命周期。 我们可以显式地写出来:

let s: &'static str = "I have a static lifetime.";

同时使用泛型,trait和生命周期

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest_with_an_announcement(
        string1.as_str(),
        string2,
        "Today is someone's birthday!",
    );
    println!("The longest string is {}", result);
}

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

下一章:Rust 哈希映射

哈希指的就是键值对。 创建一个哈希映射 下面的代码中创建了一个hash映射,rust推导了hashmap的类型: fn main() { use std::collections::HashMap; let mut ...