Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

使用生命周期验证引用

在本章中,我们将讨论生命周期——X 语言的一个特性,它允许我们确保引用的有效性,而不仅仅是所有权。

我们已经了解了借用,并且知道 X 语言有一个借用检查器,它比较作用域以确保所有借用都是有效的。

生命周期防止悬空引用

生命周期的主要目标是防止悬空引用,这会导致程序引用它不应该引用的数据。考虑下面的程序,它有一个外部作用域和一个内部作用域。

let r;
{
  let x = 5;
  r = &x;
}  // x 超出作用域

println("r: {}", r);  // 错误!r 引用了一个已被释放的值

在这里,我们尝试在 x 超出作用域后使用引用 r。X 语言会阻止这种情况,使用其借用检查器确保引用在被引用的数据超出作用域之前不会超出作用域。

生命周期注解语法

生命周期注解不会改变任何引用的存活时间。相反,它们描述了多个引用的生命周期之间的关系,而不影响生命周期。就像当函数接受具有泛型类型参数的任何类型时一样,函数可以通过声明泛型生命周期参数来接受具有任何生命周期的引用。

生命周期注解有一个稍微不寻常的语法:生命周期参数的名称必须以撇号(')开头,并且通常全是小写和短的,就像泛型类型一样。大多数人使用 'a 作为第一个生命周期参数。让我们看看生命周期注解的语法在各个位置的样子。

首先,让我们回顾一下,我们可以通过生命周期注解来命名引用的生命周期。

// 没有生命周期注解
function first_word(s: &String) -> &str {
  // ...
}

// 带有生命周期注解
function first_word<'a>(s: &'a String) -> &'a str {
  // ...
}

函数签名中的生命周期注解与泛型类型参数相关联:它们在函数名称后面的尖括号中声明。

生命周期注解的目的是告诉借用检查器多个引用的生命周期之间的关系。我们想要表达的约束是,只要生命周期 'a 存在,参数和返回值都必须有效。

函数签名中的生命周期

让我们看一些生命周期注解的例子:

// 一个没有生命周期参数的函数
// 我们返回与输入相同生命周期的引用
function longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  if x.len() > y.len() {
    x
  } else {
    y
  }
}

在这个例子中,我们有一个接受两个字符串切片并返回一个字符串切片的函数。生命周期注解表示返回的引用将与两个输入引用中寿命较短的那个一样长。这是有道理的,因为我们不知道我们会在运行时返回 x 还是 y

当我们从函数返回一个引用时,返回类型的生命周期参数需要与其中一个参数的生命周期参数匹配。如果返回的引用不引用任何参数,它必须引用函数内部创建的值,这将是一个悬空引用,因为该值将在函数结束时超出作用域。

// 这是一个错误的尝试,返回一个引用
// 到函数内部创建的值
function longest<'a>(x: &str, y: &str) -> &'a str {
  let result = String::from("really long string");
  result.as_str()  // 错误!result 在函数结束时超出作用域
}

即使我们在这个函数中有一个生命周期参数 'a,这也不会编译,因为返回值没有与任何参数相关联。问题是 result 在函数结束时超出作用域,所以我们不能从函数中返回对它的引用。

最好的解决方案是返回一个拥有的数据类型而不是引用,这样调用函数就会负责清理值。

结构体定义中的生命周期

到目前为止,我们定义的所有结构体都只拥有类型。我们也可以定义包含引用的结构体,但在这种情况下,我们需要在结构体定义中每个引用的类型上添加生命周期注解。

type ImportantExcerpt<'a> = {
  part: &'a str
}

这个结构体有一个字段 part,它包含一个字符串切片——这是一个引用。与泛型类型一样,我们在结构体名称后面的尖括号中声明泛型生命周期参数的名称,以便我们可以在结构体定义中使用生命周期参数。

这个注解意味着 ImportantExcerpt 的实例不能比它在 part 字段中持有的引用存活时间更长。

下面是一个使用这个结构体的示例:

let novel = String::from("这是一个很长的小说。它有很多句子...")

let first_sentence = novel.split('.').next().expect("找不到 '.'")

let i = {
  part: first_sentence
}

println("重要摘录: {}", i.part)

在这里,我们创建了一个包含对 novel 字符串切片的引用的 ImportantExcerpt 实例。novelImportantExcerpt 创建之前就存在了,并且 novelImportantExcerpt 超出作用域之后才超出作用域,所以 ImportantExcerpt 中的引用是有效的。

生命周期省略

到目前为止,你已经了解到,每次使用引用时都需要显式标注生命周期,这可能会很重复和烦人。好消息是,对于某些常见情况,X 语言允许我们省略生命周期注解,使用一组称为生命周期省略规则的模式。

让我们看看这些规则是如何工作的。首先,一些定义:

  • 输入生命周期:函数或方法参数上的生命周期
  • 输出生命周期:函数或方法返回值上的生命周期

生命周期省略规则是一组借用检查器应用的规则,以确定何时可以省略生命周期注解。这些规则不是完整的推断;它们是一组特定的情况,如果你的代码符合这些情况,你就不需要编写生命周期。

让我们看看三个生命周期省略规则。第一条规则适用于输入生命周期,第二和第三条规则适用于输出生命周期。

规则 1

分配给每个引用参数的生命周期都是它自己的生命周期。换句话说,一个有一个参数的函数得到一个生命周期参数:function foo<'a>(x: &'a i32);有两个参数的函数得到两个独立的生命周期参数:function foo<'a, 'b>(x: &'a i32, y: &'b i32);等等。

规则 2

如果只有一个输入生命周期参数,那么该生命周期被分配给所有输出生命周期参数:function foo<'a>(x: &'a i32) -> &'a i32

规则 3

如果有多个输入生命周期参数,但其中一个是 &self&mut self,因为这是一个方法,那么 self 的生命周期被分配给所有输出生命周期参数。

让我们看看这些规则在实践中是如何工作的。让我们从我们在本章早些时候使用的 first_word 函数开始:

// 没有生命周期注解:
function first_word(s: &String) -> &str {
  // ...
}

// 应用规则后:
function first_word<'a>(s: &'a String) -> &'a str {
  // ...
}

规则 1 说我们给每个引用参数自己的生命周期。让我们称之为 'a,就像往常一样。

规则 2 说,因为只有一个输入生命周期参数,输出生命周期也被赋值为 'a,所以签名现在看起来像我们在前面的例子中看到的那样。

让我们看看当我们以 longest 函数开始时会发生什么,它没有生命周期注解:

// 没有生命周期注解:
function longest(x: &str, y: &str) -> &str {
  // ...
}

// 应用规则 1 后:
function longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
  // ...
}

// 但是规则 2 不适用,因为有两个输入生命周期,而不是一个。
// 所以我们需要显式注解生命周期!

规则 1 适用,因为我们有两个引用参数,所以我们给每个参数自己的生命周期。但是规则 2 不适用,因为有多个输入生命周期。规则 3 也不适用,因为 longest 是一个函数而不是方法。在所有规则都应用完之后,我们仍然不知道返回类型的生命周期是什么!这就是为什么在这种情况下我们需要显式注解生命周期的原因。

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

当我们在具有生命周期的结构体上实现方法时,我们可以再次省略生命周期注解。让我们看看:

type ImportantExcerpt<'a> = {
  part: &'a str
}

impl<'a> ImportantExcerpt<'a> {
  function level(self: &Self) -> integer {
    3
  }

  function announce_and_return_part(self: &Self, announcement: &str) -> &str {
    println("注意!{}", announcement)
    self.part
  }
}

首先,注意我们不必在 impl 之后声明生命周期,因为我们可以让编译器根据结构体定义推断它们。

第一个方法 level,不需要任何生命周期注解,因为规则 3 适用(这是一个方法,所以我们可以省略生命周期注解)。

第二个方法 announce_and_return_part,有两个参数和一个返回类型。因为这是一个方法,规则 3 适用,所以我们不需要在这个方法中注解生命周期!让我们看看为什么,首先不写生命周期注解:

function announce_and_return_part(self: &Self, announcement: &str) -> &str {
  // ...
}

规则 1 说我们给 selfannouncement 都分配自己的生命周期。规则 3 说,因为 self 是一个参数,返回类型的生命周期与 self 相同,所以我们可以省略所有生命周期注解!太棒了!

静态生命周期

一个特殊的生命周期值得讨论:'static,它表示整个程序的持续时间。例如,所有字符串字面量都有 'static 生命周期,我们可以注解如下:

let s: &'static str = "我有一个静态生命周期";

这个字符串的文本直接存储在程序的二进制文件中,它总是可用的。因此,所有字符串字面量的生命周期都是 'static

在使用 'static 生命周期注解之前,先想想你所引用的引用是否真的会在程序的整个生命周期内都存在,以及你是否希望它这样做。大多数情况下,问题在于尝试创建一个悬空引用或可用生命周期不匹配;在这些情况下,解决方案是修复这些问题,而不是指定 'static 生命周期。

总结

生命周期是让 X 语言的借用检查器在没有垃圾回收的情况下工作的一部分,同时确保引用是有效的。尽管大多数时候生命周期是隐式和推断的,但我们必须在多个引用的生命周期可能以不同方式相互关联时注解它们。

通过显式注解函数签名中的生命周期,我们告诉编译器返回的引用将与参数中最短的引用一样长。这确保了我们不会意外创建悬空引用或其他无效引用!