rust 快速入门——14 生命周期

144 阅读22分钟

[!|center] 普若哥们儿

github.com/wu-hongbing…

gitee.com/wuhongbing/…

生命周期

本章的内容并不容易理解,这里先把一些要点罗列在此,便于读者阅读本章时对照理解。

  • 本章讨论的 Rust 生命周期 是为了保证引用类型的变量有效时,被引用的变量必须是有效的,从而避免悬垂引用。因此生命周期参数只会标注在引用类型上。
  • 生命周期标注相当于一个泛型,编译器编译时根据内部的上下文作用域值进行替换,进而进行合规性检查。
  • 即 Rust 编译器可以通过作用范围来确定引用是否合法,进而防止 悬垂引用,但是对于函数调用或者是结构体的构造,Rust 编译器就无法通过上下文来进行检查了 (因为每次函数调用或结构体构造使用的引用都可能不同),所以需要生命周期标注,它的作用是让编译器按照标注指定的关系对引用进行检查。
  • 函数签名的生命周期标注
    • 规则:
      • 对引用类型的返回值及其依赖的引用类型参数标注相同的生命周期参数
    • 理解:
      • 只有可能返回值依赖参数,不可能参数依赖返回值;
      • 如果存在返回值依赖参数的情况,返回值生命周期不能长于依赖参数的生命周期;
      • 生命周期长没有问题,生命周期短才可能出现问题,因此要求返回值生命周期必须小于所依赖的参数生命周期中的最小者;将返回值与其依赖的参数标注相同的生命周期参数就表达了这个意思;
      • 如果函数逻辑复杂,编译器难以确定依赖关系,因此需要标注,以帮助编译器确定依赖关系;
      • 函数签名的生命周期标注是函数的编写者制定的约定;
      • 编译器检查代码中变量生命周期有效性是否符合标注的生命周期;
      • 返回值可以是元组,因此存在多个参数标注不同的生命周期参数的可能;
  • 结构体对象生命周期标注规则:
    • 规则:
      • 为结构体中的所有引用类型成员标注生命周期参数,可相同可不同;
    • 理解:
      • 结构体对象不能长于任何一个成员的生命周期,否则通过结构体对象访问一个失效的引用是危险的;
      • 存在结构体成员生命周期参数不同的情况,参考示例;

生命周期确保引用有效

Rust 中的每一个引用都有其 生命周期lifetime),也就是引用保持有效的作用域。大部分时候生命周期是隐含并可以推断的,正如大部分时候类型也是可以推断的一样。类似于因为有多种可能类型的时候必须注明类型,也会出现引用的生命周期需要注明的情况。Rust 使用泛型生命周期参数来注明生命周期,确保运行时实际使用的引用绝对是有效的。

大部分语言没有 生命周期注解 的概念,所以这可能感觉起来有些陌生。

函数中的泛型生命周期

下例中 longest 函数返回两个字符串 slice 中较长者,代码应该会打印出 The longest string is uvwxyz

fn main() {
    let string1: String = String::from("abcd");
    let result: &str;
    {
        let string2: String = String::from("uvwxyz");
        result = longest(string1.as_str(), string2.as_str());
    } //string2 生命周期结束
    println!("The longest string is {}", result);
}

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

注意这个函数获取作为引用的字符串 slice,而不是字符串,因为我们不希望 longest 函数获取参数的所有权。

例子编译会出现如下有关生命周期的错误:

11 | 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`
help: consider introducing a named lifetime parameter
   |
11 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
   |           ++++     ++          ++          ++

提示文本揭示了返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向 xy。事实上我们也不知道,因为函数体中 if 块返回一个 x 的引用而 else 块返回一个 y 的引用!

longest 函数的返回值类型为一个引用 &str,但是编译器无法预知函数返回的是对哪个变量的引用,也就缺乏基于生命周期进行有效性检查的依据,也就无法进行生命周期有效性检查,这对 Rust 编译器来说是无法忍受的。

例子中 longest 函数返回实际上是 string2 的引用,在第 7 行,string2 出了语句块,生命周期结束了,堆内存中的内容被释放,result 引用的内容也就不存在了,第 8 行使用 result 是非法的。

为了修复这个错误,我们将增加泛型生命周期参数来定义引用间的关系以便 Rust 的借用检查器可以进行分析。

生命周期注解语法

生命周期注解并不改变任何引用的生命周期的长短相反它们描述了多个引用生命周期相互的关系。与函数签名中指定了泛型类型参数后就可以接受任何类型一样,当指定了泛型生命周期后函数也能接受任何生命周期的引用。

生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号(')开头,其名称通常全是小写,类似于泛型其名称,非常短。大多数人使用 'a 作为第一个生命周期注解。生命周期参数注解位于引用的 & 之后,并有一个空格来将引用类型与生命周期注解分隔开。

这里有一些例子:我们有一个没有生命周期参数的 i32 的引用,一个有叫做 'a 的生命周期参数的 i32 的引用,和一个生命周期也是 'ai32 的可变引用:

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

单个的生命周期注解本身没有多少意义,因为生命周期注解的目的是告诉 Rust 多个引用的泛型生命周期参数是如何相互联系的

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

为了在函数签名中使用生命周期注解,需要在函数名和参数列表间的尖括号中声明泛型生命周期参数,就像泛型类型(type)参数一样。

下例中,我们希望 longest 函数签名表达如下限制:两个参数和返回的引用的生命周期是相关的:返回值的引用存活期要长于两个参数的引用。就像下例中在每个引用中都加上了 'a 那样。

fn main() {
    let string1: String = String::from("abcd");
    let result: &str;
    {
        let string2: String = String::from("uvwxyz");
        result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {}", result);  // 这条语句移到这里,string1和string2的生命周期长度都是够的
    } //string2 生命周期结束
}

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

这段代码能够编译并会产生我们希望得到的结果。函数签名中的生命周期 'a的实际含义是 longest 函数返回的引用的生命周期与函数参数所引用的值的生命周期的较小者一致

记住通过在函数签名中指定生命周期参数时,我们并没有改变任何传入值或返回值的生命周期,而是指出任何不满足这个约束条件的值都将被借用检查器拒绝。注意 longest 函数并不需要知道 xy 具体会存在多久,而只需要知道有某个可以被 'a 替代的作用域将会满足这个签名。

生命周期注解出现在函数签名中,而不存在于函数体中的任何代码中。生命周期注解成为了函数约定的一部分,非常像签名中的类型。让函数签名包含生命周期约定意味着 Rust 编译器的工作变得更简单了,可以更准确地指出代码存在问题的部分。

当具体的引用被传递给 longest 时,'a 所替代的具体生命周期是 x 的作用域与 y 的作用域相重叠的那一部分。换一种说法就是泛型生命周期 'a 的具体生命周期等同于 xy 的生命周期中较小的那一个。因为我们用相同的生命周期参数 'a 标注了返回的引用值,所以返回的引用值就能保证在 xy 中较短的那个生命周期结束之前保持有效。

让我们看看如何通过传递拥有不同具体生命周期的引用来限制 longest 函数的使用。

下面的例子揭示了 result 的引用的生命周期必须是两个参数中较短的那个。以下代码将使用了变量 resultprintln! 移动到内部作用域之外。注意下例中的代码不能通过编译:

fn main() {
    let string1: String = String::from("abcd");
    let result: &str;
    {
        let string2: String = String::from("uvwxyz");
        result = longest(string1.as_str(), string2.as_str());
    } //string2 生命周期结束
    println!("The longest string is {}", result);
}

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

如果尝试编译会出现如下错误:

error[E0597]: `string2` does not live long enough
5 |         let string2: String = String::from("uvwxyz");
  |               ------- binding `string2` declared here
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^ borrowed value does not live long enough
7 |     } //string2 生命周期结束
  |       - `string2` dropped here while still borrowed
8 |     println!("The longest string is {}", result);
  |                                            ------ borrow later used here   

错误表明为了保证 println! 中的 result 是有效的,string2 需要直到外部作用域结束都是有效的。Rust 知道这些是因为(longest)函数的参数和返回值都使用了相同的生命周期参数 'a

深入理解生命周期

指定生命周期参数的正确方式依赖函数实现的具体功能。例如,如果将 longest 函数的实现修改为总是返回第一个参数而不是最长的字符串 slice,就不需要为参数 y 指定一个生命周期。如下代码将能够编译:

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

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

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

我们为参数 x 和返回值指定了生命周期参数 'a,不过没有为参数 y 指定,因为 y 的生命周期与参数 x 和返回值的生命周期没有任何关系。

当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。如果返回的引用 没有 指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值。然而它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。尝试考虑这个并不能编译的 longest 函数实现:

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<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str() // 悬垂引用
}

即便我们为返回值指定了生命周期参数 'a,这个实现却编译失败了,因为返回值的生命周期与参数完全没有关联。这里是会出现的错误信息:

11 |     result.as_str()
   |     ^^^^^^^^^^^^^^^ returns a reference to data owned by the current function

出现的问题是 resultlongest 函数的结尾将离开作用域并被清理,而我们尝试从函数返回一个 result 的引用。无法指定生命周期参数来改变悬垂引用,而且 Rust 也不允许我们创建一个悬垂引用。在这种情况,最好的解决方案是返回一个有所有权的数据类型而不是一个引用,这样函数调用者就需要负责清理这个值了。

综上,生命周期语法是用于将函数的多个参数与其返回值的生命周期进行关联。一旦它们形成了某种关联,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。

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

目前为止,我们定义的结构体全都包含拥有所有权的类型。也可以定义包含引用的结构体,不过这需要为结构体定义中的每一个引用添加生命周期注解。示例中有一个存放了一个字符串 slice 的结构体 ImportantExcerpt

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 实例中的引用是有效的。

结构体存在多个字段时,字段可以有不同的生命周期标注,这是 Annotating lifetime of references in a struct 中的例子:

struct S<'a,'b> {
    x: &'a i32, // 标注'a
    y: &'b i32, // 标注'b
}

fn main() {
    let x = 10;
    let r: &i32;
    {
        let y = 20;
        {
            let s = S { x: &x, y: &y };
            r = s.x;
        }  // s 生命周期结束
    } // y 生命周期结束
    println!("{}", r);
}

如果结构体 S 使用单个生命周期标注,则编译失败。

struct S<'a> {
    x: &'a i32,
    y: &'a i32,
}

例子中的生命周期参数的确定和赋值可以用下面的伪代码说明:

struct S<'a,'b> {
    x: &'a i32,
    y: &'b i32
}
fn main() {
    'a {  // 'a 生命周期开始
      let x = 10;
      let r : &'b i32;
      'b {	// 'b 生命周期开始
          let y = 20;
          {
              let s : S<'b> = S { x: &'a x, y: &'b y };
              r = s.x;
          }
      } // 'b 生命周期结束
      println!("{}", r);
    } // 'a 生命周期结束
}

实参由当前作用域作为标志,通过作用域范围的包含关系检验是否满足结构体生命周期参数要求。

下面的例子展示了结构体字段生命周期可以不同:

#[derive(Debug)]
struct Own {
    first: String,
    second: String,
}

fn main() {
    let my: Own;
    let first = "first".to_string();
    {
        let second = "second".to_string();
        my = Own {
            first: first,
            second: second,
        }
    }
    std::mem::drop(my.second); // 强制释放 my.second
    println!("{}", my.first); // 另一个字段 my.first 仍然有效
    // println!("{:?}",my); // 结构体 my 不完整,不可使用!
}

第 17 行通过标准库强制释放了一个字段的数据,只有 first 字段是有效的。

生命周期省略

现在我们已经知道,需要为那些使用了引用的函数或结构体标注生命周期。但是下例没有生命周期注解却能编译成功:

// 查找第一个单词
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 main() {
    let my_string_literal = "hello world";
    let word = first_word(my_string_literal);
    println!("the first word is: {}", word);
}

这个函数没有生命周期注解却能编译是由于一些历史原因:在早期版本(pre-1.0)的 Rust 中,这的确是不能编译的,每一个引用都必须有明确的生命周期,那时的函数签名将会写成这样:

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

Rust 团队发现在有些模式下很容易推断出引用的生命周期,Rust 团队就把这些模式编码进了 Rust 编译器中,这样 Rust 借用检查器在这些情况下就能推断出生命周期而不需要强制程序员显式的增加注解。未来更多的模式被添加到编译器中是完全可能的,未来只会需要更少的生命周期注解

被编码进 Rust 引用分析的模式被称为 生命周期省略规则lifetime elision rules),如果代码符合这些规则,就无需显式指定生命周期。

省略规则并不提供完整的推断:如果 Rust 在明确遵守这些规则的前提下变量的生命周期仍然是模棱两可的话,编译器就不会继续推断剩余引用的生命周期应该是什么,而是给出编译错误,要开发人员求显式地增加生命周期注解。

注意,并不是说代码符合生命周期省略规则就能保证不会发生有引用的生命周期而引起的错误;而是说,代码符合生命周期省略规则的话,Rust 就会为其自动生成一个生命周期标注,为开发人员省去不必要的麻烦。

函数或方法的参数的生命周期被称为 输入生命周期input lifetimes),而返回值的生命周期被称为 输出生命周期output lifetimes)。

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

  • 第一条规则是编译器为每一个引用参数都分配一个生命周期参数。换句话说就是,函数有一个引用参数就有一个生命周期参数: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),那么所有输出生命周期参数被赋予 self 的生命周期。第三条规则使得方法更容易读写,因为只需更少的符号。

现在假设我们自己就是编译器,并应用这些规则来计算示例中 first_word 函数签名中的引用的生命周期。开始时签名中的引用并没有关联任何生命周期:

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

接着编译器应用第一条规则,也就是每个引用参数都有其自己的生命周期。我们像往常一样称之为 'a,所以现在签名看起来像这样:

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

对于第二条规则,因为这里正好只有一个输入生命周期参数所以是适用的。第二条规则表明输入参数的生命周期将被赋予输出生命周期参数,所以现在签名看起来像这样:

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

现在这个函数签名中的所有引用都有了生命周期,这样,编译器就推断出了函数签名中的生命周期标注,而无须程序员标注这个函数签名中的生命周期。

让我们再看看另一个例子,这次我们从示例中没有生命周期参数的 longest 函数开始:

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

应用第一条规则:每个引用参数都有其自己的生命周期,这次有两个参数,所以就有两个(不同的)生命周期:

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

再来应用第二条规则,因为函数存在多个输入生命周期,它并不适用于这种情况。再来看第三条规则,它同样也不适用,这是因为没有 self 参数。应用了三个规则之后编译器还没有计算出返回值的生命周期,无法推断出所有引用的生命周期,于是编译器给出现错误的原因:编译器使用所有已知的生命周期省略规则,仍不能计算出签名中所有引用的生命周期。

因为第三条规则真正能够适用的就只有方法签名,现在就让我们看看那种情况中的生命周期,并看看为什么这条规则意味着我们经常不需要在方法签名中标注生命周期

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

当为带有生命周期的结构体实现方法时,其语法依然类似泛型类型参数的语法。我们在哪里声明和使用生命周期参数,取决于它们是与结构体字段相关还是与方法参数和返回值相关。

为结构体实现方法时,结构体字段的生命周期必须总是在 impl 关键字之后声明并在结构体名称之后被使用,因为这些生命周期是结构体类型的一部分。

impl 块里的方法签名中,引用可能与结构体字段中的引用相关联,也可能是独立的。另外,生命周期省略规则也经常让我们无需在方法签名中使用生命周期注解。让我们看看一些使用示例中定义的结构体 ImportantExcerpt 的例子。

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

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

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

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

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

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

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

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

静态生命周期

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

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

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

你可能在错误信息的帮助文本中见过使用 'static 生命周期的建议,不过将引用指定为 'static 之前,思考一下这个引用是否真的在整个程序的生命周期里都有效,以及你是否希望它存在得这么久。大部分情况中,推荐 'static 生命周期的错误信息都是尝试创建一个悬垂引用或者可用的生命周期不匹配的结果。在这种情况下的解决方案是修复这些问题而不是指定一个 'static 的生命周期。

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

让我们简要的看一下在同一函数中指定泛型类型参数、trait bounds 和生命周期的语法!

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

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