原文链接:https://doc.rust-lang.org/nomicon/lifetimes.html

生命周期

Rust在整个生命周期里强制执行生命周期的规则。生命周期说白了就是作用域的名字。每一个引用以及包含引用的数据结构,都要有一个生命周期来指定它保持有效的作用域。

在函数体内,Rust通常不需要你显式地给生命周期起名字。这是因为在本地上下文里,一般没有必要关注生命周期。Rust知道程序的全部信息,从而可以完美地执行各种操作。它可能会引入许多匿名或者临时的作用域让程序顺利执行。

但是如果你要跨出函数的边界,就需要关心生命周期了。生命周期用这样的符号表示:'a,'static。为了更清晰地了解生命周期,我们假设我们可以为生命周期打标签,去掉本章所有例子的语法糖。

最开始,我们的示例代码对作用域和生命周期使用了很激进的语法糖特性——甜得像玉米糖浆一样,因为把所有的东西都显式地写出来实在很讨厌。所有的Rust代码都采用比较激进的理论以省略“显而易见”的东西。

一个特别有意思的语法糖是,每一个let表达式都隐式引入了一个作用域。大多数情况下,这一点并不重要。但是当变量之间互相引用的时候,这就很重要了。举个简单的例子,我们彻底去掉下面这段代码的语法糖:

  1. let x = 0;
  2. let y = &x;
  3. let z= &y;

借用检查器通常会尽可能减少生命周期的范围,所以去掉语法糖后的代码大概像这样:

  1. // 注意:'a: { 和 &'b x 不是合法的语法
  2. 'a: {
  3. let x: i32 = 0;
  4. 'b: {
  5. // 生命周期是'b,因为这就足够了
  6. let y: &'b i32 = &'b x;
  7. 'c: {
  8. // 'c也一样
  9. let z: &'c &'b i32 = &'c y;
  10. }
  11. }
  12. }

哇!这样的写法……太可怕了。我们先停下来感谢Rust把这一切都简化掉了。

将引用传递到作用域以外会导致生命周期扩大:

  1. let x = 0;
  2. let z;
  3. let y = &x;
  4. z = y;
  1. 'a: {
  2. let x: i32 = 0;
  3. 'b: {
  4. let z: &'b i32;
  5. 'c: {
  6. // 必须使用'b,因为引用被传递到了'b的作用域
  7. let y: &'b i32 = &'b x;
  8. z = y;
  9. }
  10. }
  11. }

示例:引用超出被引用内容生命周期

好了,让我们再看一遍曾经举过的一个例子:

  1. fn as_str(data: &u32) -> &str {
  2. let s = format!("{}", data);
  3. &s
  4. }

去掉语法糖:

  1. fn as_str<'a>(data: &'a u32) -> &'a str {
  2. 'b: {
  3. let s = format!("{}", data);
  4. return &'a s;
  5. }
  6. }

函数as_str的签名里接受了一个带有生命周期的u32类型的引用,并且保证会返回一个生命周期一样长的str类型的引用。从这个签名我们就已经可以看出问题了。它表示我们必须到那个u32引用的作用域,或者比它还要早的作用域里去找一个str。这就有点不合理了。

接下来我们生成一个字符串s,然后返回它的引用。我们的函数要求这个引用的有效期不能小于'a,那是我们给引用指定的生命周期。不幸的是,s是在作用域’b里面定义的。除非’b包含’a这个函数才可能是正确的——而这显然不可能,因为’a必须包含它所调用的函数。这样我们创建了一个生命周期超出被引用内容的引用,这明显违背了之前提到的引用的第一条规则。编译器十分感动然后拒绝了我们。

我们扩展一下这个例子,一边看得更清楚:

  1. fn as_str<'a>(data: &'a u32) -> &'a str {
  2. 'b: {
  3. let s = format!("{}", data);
  4. return &'a s;
  5. }
  6. }
  7. fn main() {
  8. 'c: {
  9. let x: u32 = 0;
  10. 'd: {
  11. // 这里引入了一个匿名作用域,因为借用不需要在整个x的作用域内生效
  12. // as_str的返回值必须引用一个在函数调用前就存在的str
  13. // 显然事实不是这样的。
  14. println!("{}", as_str::<'d>(&'d x));
  15. }
  16. }
  17. }

完蛋了!

当然,这个函数的正确写法应该是这样的。

  1. fn to_string(data: &u32) -> String {
  2. format!("{}", data)
  3. }

我们必须创建一个值然后连同它的所有权一起返回。除非一个字符串是&'a u32的成员,我们才能返回&'a str,显然事情并不是这样的。

(其实我们也可以返回一个字符串的字面量,它是一个全局的变量,可以认为是处于栈的底部。尽管这样极大限制了函数的使用场合。)

示例:存在可变引用的别名

在看另一个老的例子:

  1. let mut data = vec![1, 2,3];
  2. let x = &data[0];
  3. data.push(4);
  4. println!("{}", x);
  1. 'a: {
  2. let mut data: Vec<i32> = vec![1, 2, 3];
  3. 'b: {
  4. // 对于这个借用来说,'b已经足够大了
  5. // (借用只需要在println!中生效即可)
  6. let x: &'b i32 = Index::index::<'b>(&'b data, 0);
  7. 'c: {
  8. // 引入一个临时作用域,因为&mut不需要存在更长时间
  9. Vec::push(&'c mut data, e);
  10. }
  11. println!("{}", x);
  12. }
  13. }

这里的问题更加微妙也更有趣。我们希望Rust出于如下的原因拒绝编译这段代码:我们有一个有效的指向data的内部数据的引用x,而同时又创建了一个data的可变引用用于执行push。也就是说出现了可变引用的别名,这违背了引用的第二条规则。

但是Rust其实并非因为这个原因判断这段代码有问题。Rust不知道xdata的子内容的引用,它其实完全不知道Vec的内部是什么样子的。它只知道x必须在'b范围内有效,这样才能打印其中的内容。函数Index::index的签名因此要求传递的data的引用也必须在'b的范围内有效。当我们调用push的时候,Rust发现我们要创建一个&'c mut data。它知道'c是包含在'b以内的,因为&'b data还存活着,所以它拒绝了这段程序。

我们看到了生命周期系统要比引用的保护措施更加简单粗暴。大多数情况下这也没什么,它让我们不用没完没了地向编译器解释我们的程序。但是这也意味着许多语义上正确的程序会被编译器拒绝,因为生命周期的规则太死板了。