Skip to content

生命周期

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

在这个函数中,编译器不能确定x、y的生命周期

 --> src/main.rs:1:32
  |
1 | fn longest(x: &str, y:&str) -> &str {
  |               ----    ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`

生命周期注解语法

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

例如如果函数有一个生命周期 'ai32 的引用的参数 first。还有另一个同样是生命周期 'ai32 的引用的参数 second。这两个生命周期注解意味着引用 firstsecond 必须与这泛型生命周期存在得一样久。

通过这种方式,函数可以确保返回的引用的生命周期与输入的引用一致,避免了悬垂引用的问题。

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

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

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

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

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

这里的 main 函数创建了一个 ImportantExcerpt 的实例,它存放了变量 novel 所拥有的 String 的第一个句子的引用。novel 的数据在 ImportantExcerpt 实例创建之前就存在。另外,直到 ImportantExcerpt 离开作用域之后 novel 都不会离开作用域,所以 ImportantExcerpt 实例中的引用是有效的。

生命周期省略规则

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[..]
}

编译器采用三条规则来判断引用何时不需要明确的注解。第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。这些规则适用于 fn 定义,以及 impl 块。

[!note] 第一条规则是编译器为每一个引用参数都分配一个生命周期参数。换句话说就是,函数有一个引用参数的就有一个生命周期参数:fn foo<'a>(x: &'a i32),有两个引用参数的函数就有两个不同的生命周期参数,fn foo<'a, 'b>(x: &'a i32, y: &'b i32),依此类推。

第二条规则是如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数:fn foo<'a>(x: &'a i32) -> &'a i32

第三条规则是如果方法有多个输入生命周期参数并且其中一个参数是 &self&mut self,说明是个对象的方法 (method)(译者注:这里涉及 rust 的面向对象参见 17 章),那么所有输出生命周期参数被赋予 self 的生命周期。第三条规则使得方法更容易读写,因为只需更少的符号。

根据上面三条规则

可将

fn first_word(s: &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 {

方法定义的生命周期注解

首先,这里有一个方法 level。其唯一的参数是 self 的引用,而且返回值只是一个 i32,并不引用任何值:

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

impl 之后和类型名称之后的生命周期参数是必要的,不过因为第一条生命周期规则我们并不必须标注 self 引用的生命周期。

这里是一个适用于第三条生命周期省略规则的例子:

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

这里有两个输入生命周期,所以 Rust 应用第一条生命周期省略规则并给予 &selfannouncement 他们各自的生命周期。接着,因为其中一个参数是 &self,返回值类型被赋予了 &self 的生命周期,这样所有的生命周期都被计算出来了。

静态生命周期

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

这里有一种特殊的生命周期值得讨论:'static,其生命周期能够存活于整个程序期间。所有的字符串字面值都拥有 'static 生命周期,我们也可以选择像下面这样标注出来:

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

这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的。因此所有的字符串字面值都是 'static 的。

大部分情况中,推荐 'static 生命周期的错误信息都是尝试创建一个悬垂引用或者可用的生命周期不匹配的结果。在这种情况下的解决方案是修复这些问题而不是指定一个 'static 的生命周期。

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

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

这个是示例 10-21 中那个返回两个字符串 slice 中较长者的 longest 函数,不过带有一个额外的参数 annann 的类型是泛型 T,它可以被放入任何实现了 where 从句中指定的 Display trait 的类型。这个额外的参数会使用 {} 打印,这也就是为什么 Display trait bound 是必须的。因为生命周期也是泛型,所以生命周期参数 'a 和泛型类型参数 T 都位于函数名后的同一尖括号列表中。