生命周期,简而言之就是引用的有效作用域。在大多数时候,我们无需手动的声明生命周期,因为编译器可以自动进行推导,用类型来类比下:
  • 就像编译器大部分时候可以自动推导类型 <-> 一样,编译器大多数时候也可以自动推导生命周期
  • 在多种类型存在时,编译器往往要求我们手动标明类型 <-> 当多个生命周期存在,且编译器无法推导出某个引用的生命周期时,就需要我们手动标明生命周期

生命周期的主要作用是避免悬垂引用,它会导致程序引用了本不该引用的数据。

借用检查

在编译期,Rust 会比较两个变量的生命周期,如果发现 r 明明拥有生命周期 ‘a,却引用了一个小得多的生命周期 ‘b,在这种情况下,编译器会认为我们的程序存在风险,因此拒绝运行。
  1. {
  2. let r; // ---------+-- 'a
  3. // |
  4. { // |
  5. let x = 5; // -+-- 'b |
  6. r = &x; // | |
  7. } // -+ |
  8. // |
  9. println!("r: {}", r); // |
  10. } // ---------+

如果想要编译通过,也很简单,只要 ‘b‘a 大就好。总之,x 变量只要比 r 活得久,那么 r 就能随意引用 x 且不会存在危险:

  1. {
  2. let x = 5; // ----------+-- 'b
  3. // |
  4. let r = &x; // --+-- 'a |
  5. // | |
  6. println!("r: {}", r); // | |
  7. // --+ |
  8. } // ----------+

函数中的生命周期

先看一个例子: 返回两个字符串切片中较长的那个,该函数的参数是两个字符串切片,返回值也是字符串切片:

  1. fn main() {
  2. let string1 = String::from("abcd");
  3. let string2 = "xyz";
  4. let result = longest(string1.as_str(), string2);
  5. println!("The longest string is {}", result);
  6. }
  1. fn longest(x: &str, y: &str) -> &str {
  2. if x.len() > y.len() {
  3. x
  4. } else {
  5. y
  6. }
  7. }
这段 longest 实现,非常标准优美,就连多余的 return 和分号都没有,可是现实总是给我们重重一击:
  1. error[E0106]: missing lifetime specifier
  2. --> src/main.rs:9:33
  3. |
  4. 9 | fn longest(x: &str, y: &str) -> &str {
  5. | ---- ---- ^ expected named lifetime parameter // 参数需要一个生命周期
  6. |
  7. = help: this function's return type contains a borrowed value, but the signature does not say whether it is
  8. borrowed from `x` or `y`
  9. = 帮助: 该函数的返回值是一个引用类型,但是函数签名无法说明,该引用是借用自 `x` 还是 `y`
  10. help: consider introducing a named lifetime parameter // 考虑引入一个生命周期
  11. |
  12. 9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  13. | ^^^^ ^^^^^^^ ^^^^^^^ ^^^

报错信息很明显:该函数的返回值是一个引用类型,但是函数签名无法说明,该引用是借用自 x 还是 y

解决方法:在存在多个引用时,编译器有时会无法自动推导生命周期,此时就需要我们手动去标注,通过为参数标注合适的生命周期来帮助编译器进行借用检查的分析。

生命周期标注语法

标记的生命周期只是为了取悦编译器,让编译器不要难为我们。即生命周期标注并不会改变任何引用的实际作用域 。

生命周期的语法也颇为与众不同,**** 开头,名称往往是一个单独的小写字母,大多数人都用 ‘a 来作为生命周期的名称。 如果是引用类型的参数,那么生命周期会位于引用符号 & 之后,并用一个空格来将生命周期和引用参数分隔开:
  1. &i32 // 一个引用
  2. &'a i32 // 具有显式生命周期的引用
  3. &'a mut i32 // 具有显式生命周期的可变引用

有一个函数,它的第一个参数 first 是一个指向 i32 类型的引用,具有生命周期 ‘a,该函数还有另一个参数 second,它也是指向 i32 类型的引用,并且同样具有生命周期 ‘a。此处生命周期标注仅仅说明,这两个参数** first second **至少活得和’a 一样久,至于到底活多久或者哪个活得更久,抱歉我们都无法得知

  1. fn useless<'a>(first: &'a i32, second: &'a i32) {}

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

继续之前的 longest 函数,从两个字符串切片中返回较长的那个,改写如下:
  1. fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  2. if x.len() > y.len() {
  3. x
  4. } else {
  5. y
  6. }
  7. }
需要注意的点如下:
  • 和泛型一样,使用生命周期参数,需要先声明 **<’a>**
  • xy 和返回值至少活得和 ‘a 一样久(因为返回值要么是 x,要么是 y)

还有一个结论:当把具体的引用传给 longest 时,那生命周期 ‘a 的大小就是 xy 的作用域的重合部分,换句话说,‘a** 的大小将等于 xy 中较小的那个。由于返回值的生命周期也被标记为 ‘a,因此返回值的生命周期也是 xy 中作用域较小的那个。**为了验证这个结论,我们看如下例子:

  1. fn main() {
  2. let string1 = String::from("abcd");
  3. {
  4. let string2 = String::from("xyz");
  5. let result = longest(string1.as_str(), string2.as_str());
  6. println!("The longest string is {}", result);
  7. }
  8. }
  9. fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  10. if x.len() > y.len() {
  11. x
  12. } else {
  13. y
  14. }
  15. }
以上代码能正常运行,现在来验证下上面的结论:result 的生命周期等于参数中生命周期最小的,因此要等于 string2 的生命周期,也就是说,result 要活得和 string2 一样久,观察下代码的实现,可以发现这个结论是正确的!
  1. fn main() {
  2. let string1 = String::from("long string is long");
  3. let result;
  4. {
  5. let string2 = String::from("xyz");
  6. result = longest(string1.as_str(), string2.as_str());
  7. }
  8. println!("The longest string is {}", result);
  9. }

报错如下:

  1. error[E0597]: `string2` does not live long enough
  2. --> src/main.rs:6:44
  3. |
  4. 6 | result = longest(string1.as_str(), string2.as_str());
  5. | ^^^^^^^ borrowed value does not live long enough
  6. 7 | }
  7. | - `string2` dropped here while still borrowed
  8. 8 | println!("The longest string is {}", result);
  9. | ------ borrow later used here
longest 函数中,string2 的生命周期也是 ‘a,由此说明 string2 也必须活到 println! 处,可是 string2 在代码中实际上只能活到内部语句块的花括号处 },小于它应该具备的生命周期 ‘a,因此编译出错。

深入思考生命周期标注

函数的返回值如果是一个引用类型,那么它的生命周期只会来源于

  • 函数参数的生命周期
  • 函数体中某个新建引用的生命周期
若是后者情况,就是典型的悬垂引用场景:
  1. fn longest<'a>(x: &str, y: &str) -> &'a str {
  2. let result = String::from("really long string");
  3. result.as_str()
  4. }
上面的函数的返回值就和参数 xy 没有任何关系,而是引用了函数体内创建的字符串,那么很显然,该函数会报错:
  1. error[E0515]: cannot return value referencing local variable `result` // 返回值result引用了本地的变量
  2. --> src/main.rs:11:5
  3. |
  4. 11 | result.as_str()
  5. | ------^^^^^^^^^
  6. | |
  7. | returns a value referencing data owned by the current function
  8. | `result` is borrowed here
主要问题就在于,result 在函数结束后就被释放,但是在函数结束后,对 result 的引用依然在继续。在这种情况下,没有办法指定合适的生命周期来让编译通过,因此我们也就在 Rust 中避免了悬垂引用。 那遇到这种情况该怎么办?最好的办法就是返回内部字符串的所有权,然后把字符串的所有权转移给调用者
  1. fn longest<'a>(_x: &str, _y: &str) -> String {
  2. String::from("really long string")
  3. }
  4. fn main() {
  5. let s = longest("not", "important");
  6. }
至此,可以对生命周期进行下总结:生命周期语法用来将函数的多个引用参数和返回值的作用域关联到一起,一旦关联到一起后,Rust 就拥有充分的信息来确保我们的操作是内存安全的。

结构体中的生命周期

  1. struct ImportantExcerpt<'a> {
  2. part: &'a str,
  3. }
  4. fn main() {
  5. let novel = String::from("Call me Ishmael. Some years ago...");
  6. let first_sentence = novel.split('.').next().expect("Could not find a '.'");
  7. let i = ImportantExcerpt {
  8. part: first_sentence,
  9. };
  10. }
ImportantExcerpt 结构体中有一个引用类型的字段 part,因此需要为它标注上生命周期。结构体的生命周期标注语法跟泛型参数语法很像,需要对生命周期参数进行声明 <’a>。该生命周期标注说明,结构体 **ImportantExcerpt 所引用的字符串 str** 必须比该结构体活得更久。 # 生命周期消除 实际上,对于编译器来说,每一个引用类型都有一个生命周期。哪些情况无需标注生命周期呢?接下来我们了解一下生命周期的三条消除规则。 在开始之前有几点需要注意:
  • 消除规则不是万能的,若编译器不能确定某件事是正确时,会直接判为不正确,那么你还是需要手动标注生命周期
  • 函数或者方法中,参数的生命周期被称为 **输入生命周期,返回值的生命周期被称为 输出生命周期**
编译器使用三条消除规则来确定哪些场景不需要显式地去标注生命周期。其中第一条规则应用在输入生命周期上,第二、三条应用在输出生命周期上。若编译器发现三条规则都不适用时,就会报错,提示你需要手动标注生命周期。
  1. 每一个引用参数都会获得独自的生命周期

例如一个引用参数的函数就有一个生命周期标注: fn foo<’a>(x: &’a i32),两个引用参数的有两个生命周期标注:fn foo<’a, ‘b>(x: &’a i32, y: &’b i32), 依此类推。

  1. 若只有一个输入生命周期(函数参数中只有一个引用类型),那么该生命周期会被赋给所有的输出生命周期,也就是所有返回值的生命周期都等于该输入生命周期

例如函数 fn foo(x: &i32) -> &i32,x 参数的生命周期会被自动赋给返回值 &i32,因此该函数等同于 fn foo<’a>(x: &’a i32) -> &’a i32

  1. 若存在多个输入生命周期,且其中一个是 **&self&mut self,则 &self** 的生命周期被赋给所有的输出生命周期

方法中的生命周期

先来回忆下泛型的语法:
  1. struct Point<T> {
  2. x: T,
  3. y: T,
  4. }
  5. impl<T> Point<T> {
  6. fn x(&self) -> &T {
  7. &self.x
  8. }
  9. }
实际上,为具有生命周期的结构体实现方法时,我们使用的语法跟泛型参数语法很相似:
  1. struct ImportantExcerpt<'a> {
  2. part: &'a str,
  3. }
  4. impl<'a> ImportantExcerpt<'a> {
  5. fn level(&self) -> i32 {
  6. 3
  7. }
  8. }
其中有几点需要注意的:
  • impl 中必须使用结构体的完整名称,包括 <’a>,因为生命周期标注也是结构体类型的一部分
  • 方法签名中,往往不需要标注生命周期,得益于生命周期消除的第一和第三规则

静态生命周期

在 Rust 中有一个非常特殊的生命周期,那就是 ‘static,拥有该生命周期的引用可以和整个程序活得一样久。 在之前我们学过字符串字面量,提到过它是被硬编码进 Rust 的二进制文件中,因此这些字符串变量全部具有 ‘static 的生命周期:
  1. let s: &'static str = "我没啥优点,就是活得久,嘿嘿";
static总结如下:
  • 生命周期 ‘static 意味着能和程序活得一样久,例如字符串字面量和特征对象
  • 实在遇到解决不了的生命周期标注问题,可以尝试 T: ‘static,有时候它会给你奇迹