Rust生命周期

生命周期是另一类泛型,不是确保类型有期望的行为,而是确保引用如预期一直有效。大多数时候,生命周期是隐式的,Rust会根据引用的使用方式来推断生命周期,但是一些时候引用的生命周期以一些不同方式相关联,Rust需要我们使用泛型生命周期参数来注明它们的关系。

生命周期避免了悬垂引用🔗

考虑下面的代码:

fn main() {
    let r;
    {
        let x = 5;
        r = &x;
    }
    println!("r: {}", r);
}

这段代码会报错,因为在{}中,x的生命周期结束了,但是r仍然指向x,这就是悬垂引用。

借用检查器🔗

Rust有一个借用检查器(borrow checker),他比较作用域来确保所有借用都是有效的,下面是上面代码带有生命周期的注释:

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

'a是r的生命周期,'b是x的生命周期,编译时,Rust会比较两个生命周期的大小,并发现r拥有生命周期'a,不过它引用了一个拥有生命周期'b的变量,这是不允许的,因为被引用的对象比他的引用者存在时间更短。下面是没有悬垂引用的代码例子:

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

'a的生命周期比'b的生命周期长,所以这是合法的。

函数中的泛型生命周期🔗

编写一个返回两个字符串slice中长的那个的函数:

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

这段代码不能编译,因为Rust不知道将要返回的引用是来自x还是y。编译器不知道返回的生命周期是怎么和x和y相关联的,所以它会报错。

为了解决这个问题,我们需要使用泛型生命周期参数。

生命周期注解语法🔗

生命周期注解不改变任何引用的生命周期长短,只是描述多个引用生命周期相互的关系。指定了泛型生命周期后函数也能接受任何生命周期的引用。

生命周期注解参数名称必须以(')开头,名称通常全为小写,且很短。生命周期参数注解位于引用&之后,并有一个空格来将引用类型和生命周期注解分隔开。下面是一些例子:

&i32          // 引用
&'a i32       // 带有显式生命周期的引用
&'a mut i32   // 带有显式生命周期的可变引用

单个生命周期注解本身没什么意义,因为生命周期注解是告诉Rust多个引用的泛型生命周期参数的联系的。

函数签名中的生命周期注解🔗

在函数签名中使用生命周期注解需要在函数名和参数列表间的尖括号中声明泛型生命周期参数,假如希望两个参数和返回的引用存活得一样久,则代码如下:

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

这里表示函数返回的引用的生命周期和函数参数所引用的值的生命周期的较小值一致。

具体引用被传递给longest时,被'a所替代的具体生命周期是x和y的作用域重叠的那一部分。返回的引用值能保证在x和y的作用域内有效。查看下面例子:

fn main() {
    let string1 = String::from("long string is long");
    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {}", result);
    }
}

这里string1和string2的在内部作用域都有效,所以result也有效。下面看另外的例子。

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}

在外部作用域中,string1是有效的,但是string2是无效的,为了保证println!中的result是有效的,string2也需要在外部作用域有效,所以代码无法编译。

虽然以代码的角度讲,上述例子println!中的result中是string1的引用,但是生命周期参数告诉Rust,result的生命周期不能超过string2的生命周期。

考虑如下代码:

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

这里为x和返回值指定了生命周期参数'a,但是没有为y指定生命周期,因y的生命周期与x和返回值生命周期无关。

函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。如果其返回的引用没有指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值,这是一个悬垂引用,因为函数结束时就离开了作用域,不能编译:

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

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().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence
    };
}

这个结构体有唯一字段part,它存放了字符串slice,是一个引用,必须在结构一名称后面的尖括号中声明泛型生命周期参数。这个注解意味着ImportantExcerpt实例的生命周期不能比part引用的生命周期久。

生命周期省略(Lifetime Elision)🔗

每一个引用都有一个生命周期,然而考虑以下代码:

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

这段代码的参数和返回值都是引用,虽然没有使用生命周期注解,但是可以编译,因为Rust有一个生命周期省略规则(lifetime elision rule)。 编译器通过三条规则来判断引用何时不需要明确的注解,第一条适用于输入生命周期,后面两条适用于输出生命周期:

  • 编译器为每一个引用参数都分配了一个生命周期参数。
  • 如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数。
  • 如果方法有多个输入生命周期参数,且其中一个是&self或&mut self,那么self的生命周期被赋予所有输出生命周期参数。

以下是例子:

fn first_word(s: &str) -> &str {}

这里适用第一条规则,所以是下面这样:

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

然后适用第二条规则:

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

现在编译器就可以编译了。

再看看另一个例子:

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

这里适用第一条规则,所以是下面这样:

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

然后第二条规则和第三条规则都适用,所以会出错。

ℹ️ 注意

实际上第三条规则真正能够适用的只有方法签名。

方法签名中的生命周期注解🔗

带生命周期的结构体实现方法时,我们在哪里声明和使用生命周期参数取决于它们是与结构体字段相关还是与方法参数和返回值相关。

实现方法时结构体字段的生命周期必须总是在impl关键字之后声明并在结构体名称之后被使用,下面看例子:

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

这里方法level唯一的参数是self的引用,返回值是i32,impl和类型名后面的生命周期参数是必要的,不过因为第一条生命周期规则我们不用标注self引用的生命周期。

考虑下面代码:

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

这里应用第一条规则和第三条规则,所有引用的生命周期都被计算出来了。

静态生命周期🔗

一种特殊的生命周期'static,它表示引用的生命周期与整个程序的运行时间相同。将引用指定为'static要考虑这个引用是否真的需要在整个程序生命周期都有效,而不是只是为了修复一些悬垂引用或可用生命周期不匹配的错误。

结合泛型类型参数、trait bound和生命周期🔗

考虑以下代码:

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
    }
}

这个例子同时使用了生命周期注解、泛型类型参数和trait bound。