使用生命周期验证引用

生命周期是我们一直在使用的另一种泛型。它不是确保类型具有我们想要的行为,而是确保引用在我们需要的时长内有效。

我们在 “引用与借用”第 4 章中没有讨论的一个细节是,Rust 中的每个引用都有一个生命周期,这是该引用有效的范围。大多数情况下,生命周期是隐式的和推断的,就像大多数时候类型是推断的一样。只有在存在多种类型可能的情况下,我们才必须注释类型。类似地,当引用的生命周期可能以几种不同的方式相关时,我们必须注释生命周期。Rust 要求我们使用泛型生命周期参数注释关系,以确保运行时使用的实际引用绝对有效。

注释生命周期并不是大多数其他编程语言都有的概念,因此这会让人感到陌生。虽然我们不会在本章中完整地介绍生命周期,但我们将讨论你可能会遇到的生命周期语法的常见方式,以便你熟悉这个概念。

使用生命周期防止悬空引用

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

fn main() {
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {r}");
}

清单 10-16:尝试使用值已超出作用域的引用

注意:清单 10-16、10-17 和 10-23 中的示例声明了变量,但没有给它们初始值,因此变量名存在于外部作用域中。乍一看,这可能与 Rust 没有空值相冲突。但是,如果我们尝试在使用变量之前给它一个值,我们会得到一个编译时错误,这表明 Rust 确实不允许空值。

外部作用域声明一个名为 r 的变量,没有初始值,内部作用域声明一个名为 x 的变量,初始值为 5。在内部作用域中,我们尝试将 r 的值设置为对 x 的引用。然后,内部作用域结束,我们尝试打印 r 中的值。此代码不会编译,因为 r 引用的值在我们尝试使用它之前已超出作用域。这是错误消息

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
 --> src/main.rs:6:13
  |
5 |         let x = 5;
  |             - binding `x` declared here
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `x` dropped here while still borrowed
8 |
9 |     println!("r: {r}");
  |                  --- borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

错误消息说变量 x “存活时间不够长”。原因是 x 将在第 7 行的内部作用域结束时超出作用域。但 r 对于外部作用域仍然有效;因为它的作用域更大,我们说它“存活时间更长”。如果 Rust 允许此代码工作,r 将引用在 x 超出作用域时被释放的内存,而我们尝试对 r 执行的任何操作都不会正确工作。那么 Rust 如何确定此代码无效呢?它使用借用检查器。

借用检查器

Rust 编译器有一个借用检查器,它比较作用域以确定所有借用是否有效。清单 10-17 显示了与清单 10-16 相同的代码,但带有注释,显示了变量的生命周期。

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {r}");   //          |
}                         // ---------+

清单 10-17:rx 的生命周期注释,分别命名为 'a'b

在这里,我们使用 'a 注释了 r 的生命周期,使用 'b 注释了 x 的生命周期。正如你所看到的,内部 'b 块比外部 'a 生命周期块小得多。在编译时,Rust 会比较两个生命周期的大小,并看到 r 的生命周期为 'a,但它引用的是生命周期为 'b 的内存。程序被拒绝,因为 'b'a 短:引用的主题的生命周期不如引用的长。

清单 10-18 修复了代码,使其没有悬空引用,并且可以编译而没有任何错误。

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {r}");   //   |       |
                          // --+       |
}                         // ----------+

清单 10-18:一个有效的引用,因为数据的生命周期比引用的长

在这里,x 的生命周期为 'b,在这种情况下,它比 'a 大。这意味着 r 可以引用 x,因为 Rust 知道 r 中的引用在 x 有效时始终有效。

既然你已经知道引用的生命周期是什么以及 Rust 如何分析生命周期以确保引用始终有效,那么让我们在函数的上下文中探讨参数和返回值的泛型生命周期。

函数中的泛型生命周期

我们将编写一个函数,返回两个字符串切片中较长的那个。这个函数将接受两个字符串切片并返回一个字符串切片。在我们实现 longest 函数后,清单 10-19 中的代码应打印 The longest string is abcd

文件名:src/main.rs

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

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

清单 10-19:调用 longest 函数以查找两个字符串切片中较长者的 main 函数

请注意,我们希望函数接受字符串切片(即引用),而不是字符串,因为我们不希望 longest 函数获取其参数的所有权。有关为什么我们在清单 10-19 中使用的参数是我们想要的参数的更多讨论,请参阅第 4 章中的 “作为参数的字符串切片”部分。

如果我们尝试实现如清单 10-20 所示的 longest 函数,它将无法编译。

文件名:src/main.rs

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(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

清单 10-20:longest 函数的实现,它返回两个字符串切片中较长的一个,但尚未编译

相反,我们会收到以下关于生命周期的错误

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | 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
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

帮助文本显示返回类型需要一个泛型生命周期参数,因为 Rust 无法判断要返回的引用是指向 x 还是 y。实际上,我们也不知道,因为此函数体中的 if 块返回对 x 的引用,而 else 块返回对 y 的引用!

当我们定义此函数时,我们不知道将传递给此函数的具体值,因此我们不知道是执行 if 情况还是 else 情况。我们也不知道将传入的引用的具体生命周期,因此我们无法像在清单 10-17 和 10-18 中那样查看作用域来确定我们返回的引用是否始终有效。借用检查器也无法确定这一点,因为它不知道 xy 的生命周期与返回值的生命周期有何关系。要修复此错误,我们将添加泛型生命周期参数,以定义引用之间的关系,以便借用检查器可以执行其分析。

生命周期注释语法

生命周期注释不会更改任何引用的生命周期。相反,它们描述了多个引用的生命周期之间彼此的关系,而不会影响生命周期。正如函数可以通过指定泛型类型参数来接受任何类型一样,函数可以通过指定泛型生命周期参数来接受具有任何生命周期的引用。

生命周期注释的语法略有不同:生命周期参数的名称必须以撇号 (') 开头,并且通常都是小写字母且很短,就像泛型类型一样。大多数人使用名称 'a 作为第一个生命周期注释。我们将生命周期参数注释放在引用的 & 之后,使用空格将注释与引用的类型分开。

以下是一些示例:对没有生命周期参数的 i32 的引用,对具有名为 'a 的生命周期参数的 i32 的引用,以及对具有生命周期 'ai32 的可变引用。

&i32        // a reference
&'a i32     // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime

一个生命周期注释本身并没有太多意义,因为注释旨在告诉 Rust 多个引用的泛型生命周期参数如何彼此相关。让我们研究一下生命周期注释如何在 longest 函数的上下文中彼此相关。

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

要在函数签名中使用生命周期注释,我们需要在函数名和参数列表之间的尖括号内声明泛型生命周期参数,就像我们对泛型类型参数所做的那样。

我们希望签名表达以下约束:返回的引用只要两个参数都有效,就有效。这是参数的生命周期和返回值之间的关系。我们将生命周期命名为 'a,然后将其添加到每个引用中,如清单 10-21 所示。

文件名:src/main.rs

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: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

清单 10-21:longest 函数定义,指定签名中的所有引用都必须具有相同的生命周期 'a

当我们将此代码与清单 10-19 中的 main 函数一起使用时,此代码应该可以编译并产生我们想要的结果。

函数签名现在告诉 Rust,对于某个生命周期 'a,该函数接受两个参数,这两个参数都是至少与生命周期 'a 一样长的字符串切片。函数签名还告诉 Rust,从函数返回的字符串切片将至少与生命周期 'a 一样长。在实践中,这意味着由 longest 函数返回的引用的生命周期与函数参数引用的值的生命周期中较小的生命周期相同。这些关系是我们在分析此代码时希望 Rust 使用的关系。

请记住,当我们在函数签名中指定生命周期参数时,我们不是在更改传入或返回的任何值的生命周期。相反,我们指定借用检查器应拒绝任何不遵守这些约束的值。请注意,longest 函数不需要确切知道 xy 将存活多久,只需要可以使用某些作用域替换 'a,就可以满足此签名。

在函数中注释生命周期时,注释会放在函数签名中,而不是函数体中。生命周期注释成为函数合约的一部分,很像签名中的类型。让函数签名包含生命周期合约意味着 Rust 编译器所做的分析可以更简单。如果函数的注释方式或调用方式有问题,编译器错误可以更精确地指向我们代码的一部分和约束。相反,如果 Rust 编译器对我们打算的生命周期关系进行更多推断,则编译器可能只能指向我们代码的使用,而距离问题的根源还有很长的路要走。

当我们向 longest 传递具体引用时,替代 'a 的具体生命周期是 x 的作用域与 y 的作用域重叠的部分。换句话说,泛型生命周期 'a 将获得等于 xy 生命周期中较小者的具体生命周期。由于我们使用相同的生命周期参数 'a 注释了返回的引用,因此返回的引用也将对 xy 生命周期中较短的那个有效。

让我们看一下生命周期注释如何通过传递具有不同具体生命周期的引用来限制 longest 函数。示例 10-22 是一个简单的例子。

文件名:src/main.rs

fn main() {
    let string1 = String::from("long string is long");

    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        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
    }
}

示例 10-22:将 longest 函数与具有不同具体生命周期的 String 值的引用一起使用

在此示例中,string1 在外部作用域结束之前有效,string2 在内部作用域结束之前有效,并且 result 引用在内部作用域结束之前有效的内容。运行此代码,您将看到借用检查器会批准;它将编译并打印 The longest string is long string is long

接下来,让我们尝试一个示例,该示例表明 result 中引用的生命周期必须是两个参数中较小的生命周期。我们将 result 变量的声明移到内部作用域之外,但将值的赋值保留在带有 string2 的作用域内。然后,我们将使用 resultprintln! 移到内部作用域之外,在内部作用域结束后。示例 10-23 中的代码将无法编译。

文件名:src/main.rs

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    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
    }
}

示例 10-23:尝试在 string2 超出范围后使用 result

当我们尝试编译此代码时,会收到此错误

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
5 |         let string2 = String::from("xyz");
  |             ------- binding `string2` declared here
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {result}");
  |                                     -------- borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

错误表明,为了使 result 对于 println! 语句有效,string2 需要在外部作用域结束之前都有效。Rust 知道这一点,因为我们使用相同的生命周期参数 'a 注释了函数参数和返回值的生命周期。

作为人类,我们可以查看此代码并看到 string1string2 长,因此,result 将包含对 string1 的引用。由于 string1 尚未超出作用域,因此对 string1 的引用对于 println! 语句仍然有效。但是,编译器无法看到在这种情况下该引用有效。我们告诉 Rust,longest 函数返回的引用的生命周期与传入的引用中较小的生命周期相同。因此,借用检查器不允许示例 10-23 中的代码,因为它可能具有无效的引用。

尝试设计更多实验,更改传递给 longest 函数的引用的值和生命周期,以及如何使用返回的引用。在编译之前,对您的实验是否会通过借用检查器做出假设;然后检查您是否正确!

从生命周期角度思考

你需要指定生命周期参数的方式取决于你的函数在做什么。例如,如果我们更改 longest 函数的实现以始终返回第一个参数而不是最长的字符串切片,则我们无需在 y 参数上指定生命周期。以下代码将编译

文件名:src/main.rs

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 函数的以下尝试实现,它无法编译

文件名:src/main.rs

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,此实现也将无法编译,因为返回值生命周期与参数的生命周期根本不相关。这是我们收到的错误消息

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return value referencing local variable `result`
  --> src/main.rs:11:5
   |
11 |     result.as_str()
   |     ------^^^^^^^^^
   |     |
   |     returns a value referencing data owned by the current function
   |     `result` is borrowed here

For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

问题是 result 超出作用域,并在 longest 函数结束时被清除。我们还尝试从函数返回对 result 的引用。我们无法指定可以更改悬空引用的生命周期参数,并且 Rust 不允许我们创建悬空引用。在这种情况下,最好的修复方法是返回拥有的数据类型而不是引用,以便调用函数负责清除该值。

最终,生命周期语法是关于连接函数的各种参数和返回值的生命周期。一旦它们连接起来,Rust 就有足够的信息来允许内存安全操作并禁止会创建悬空指针或以其他方式违反内存安全的操作。

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

到目前为止,我们定义的所有结构体都持有拥有的类型。我们可以定义结构体来持有引用,但在这种情况下,我们需要在结构体的定义中为每个引用添加生命周期注释。示例 10-24 有一个名为 ImportantExcerpt 的结构体,它持有字符串切片。

文件名:src/main.rs

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

示例 10-24:一个持有引用的结构体,需要生命周期注释

此结构体具有单个字段 part,该字段持有字符串切片,这是一个引用。与泛型数据类型一样,我们在结构体的名称后的尖括号内声明泛型生命周期参数的名称,以便我们可以在结构体定义的主体中使用生命周期参数。此注释表示 ImportantExcerpt 的实例不能超出其 part 字段中持有的引用的生命周期。

这里的 main 函数创建 ImportantExcerpt 结构体的一个实例,该实例持有对变量 novel 拥有的 String 的第一句话的引用。novel 中的数据在创建 ImportantExcerpt 实例之前就存在。此外,novel 不会在 ImportantExcerpt 超出范围之后才超出范围,因此 ImportantExcerpt 实例中的引用是有效的。

生命周期省略

你已经了解到每个引用都有一个生命周期,并且你需要为使用引用的函数或结构体指定生命周期参数。但是,我们在示例 4-9(在示例 10-25 中再次显示)中有一个函数,该函数在没有生命周期注释的情况下编译。

文件名:src/lib.rs

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 = String::from("hello world");

    // first_word works on slices of `String`s
    let word = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word works on slices of string literals
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

示例 10-25:我们在示例 4-9 中定义的函数,即使参数和返回类型都是引用,它也可以在没有生命周期注释的情况下编译

此函数在没有生命周期注释的情况下编译的原因是历史原因:在 Rust 的早期版本(1.0 之前)中,此代码不会编译,因为每个引用都需要一个显式的生命周期。当时,函数签名将像这样编写

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

在编写了大量 Rust 代码后,Rust 团队发现 Rust 程序员在特定情况下一遍又一遍地输入相同的生命周期注释。这些情况是可预测的,并且遵循一些确定的模式。开发人员将这些模式编程到编译器的代码中,以便借用检查器可以在这些情况下推断生命周期,而无需显式注释。

这段 Rust 历史是相关的,因为可能会出现更多确定的模式并将其添加到编译器中。将来,可能需要更少的生命周期注释。

编程到 Rust 的引用分析中的模式称为生命周期省略规则。这些不是程序员要遵循的规则;它们是编译器将考虑的一组特定情况,如果你的代码符合这些情况,则无需显式写入生命周期。

省略规则不提供完整的推断。如果在 Rust 应用这些规则后,仍然不清楚引用的生命周期是什么,则编译器不会猜测剩余引用的生命周期应该是什么。编译器不会猜测,而是会给您一个错误,您可以通过添加生命周期注释来解决该错误。

函数或方法参数的生命周期称为输入生命周期,返回值的生命周期称为输出生命周期

当没有显式注释时,编译器使用三个规则来确定引用的生命周期。第一个规则适用于输入生命周期,第二个和第三个规则适用于输出生命周期。如果编译器到达三个规则的末尾,并且仍然有无法确定生命周期的引用,则编译器将停止并显示错误。这些规则适用于 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 因为这是一个方法,则 self 的生命周期将分配给所有输出生命周期参数。第三个规则使方法更易于阅读和编写,因为需要的符号更少。

让我们假装自己是编译器。我们将应用这些规则来确定示例 10-25 中 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 函数,该函数在我们开始在示例 10-20 中使用它时没有任何生命周期参数

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

让我们应用第一个规则:每个参数都有自己的生命周期。这次我们有两个参数而不是一个,所以我们有两个生命周期

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

你可以看到第二个规则不适用,因为有多个输入生命周期。第三个规则也不适用,因为 longest 是一个函数而不是一个方法,因此没有参数是 self。在完成了所有三个规则之后,我们仍然没有弄清楚返回类型的生命周期是什么。这就是为什么我们尝试编译示例 10-20 中的代码时出现错误:编译器完成了生命周期省略规则,但仍然无法确定签名中所有引用的生命周期。

因为第三个规则实际上只适用于方法签名,所以接下来我们将在该上下文中查看生命周期,以了解为什么第三个规则意味着我们不必在方法签名中经常注释生命周期。

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

当我们在具有生命周期的结构体上实现方法时,我们使用与示例 10-11 中所示的泛型类型参数相同的语法。我们声明和使用生命周期参数的位置取决于它们是否与结构体字段或方法参数和返回值相关。

结构体字段的生命周期名称总是需要在 impl 关键字之后声明,然后在结构体名称之后使用,因为这些生命周期是结构体类型的一部分。

impl 代码块中的方法签名中,引用可能会与结构体字段中引用的生命周期相关联,也可能是独立的。此外,生命周期省略规则通常使得在方法签名中不需要生命周期注解。让我们看一些使用我们在列表 10-24 中定义的名为 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().unwrap();
    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().unwrap();
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

这里有两个输入生命周期,因此 Rust 应用第一个生命周期省略规则,并给 &selfannouncement 都赋予它们自己的生命周期。然后,由于其中一个参数是 &self,返回类型将获得 &self 的生命周期,并且所有生命周期都已得到考虑。

静态生命周期

我们需要讨论的一个特殊的生命周期是 'static,它表示受影响的引用可以在程序的整个持续时间内存在。所有字符串字面量都具有 'static 生命周期,我们可以如下注释:

#![allow(unused)]
fn main() {
let s: &'static str = "I have a static lifetime.";
}

此字符串的文本直接存储在程序的可执行文件中,该文件始终可用。因此,所有字符串字面量的生命周期都是 'static

您可能会在错误消息中看到使用 'static 生命周期的建议。但在将 'static 指定为引用的生命周期之前,请考虑您拥有的引用是否真的在程序的整个生命周期内都存在,以及您是否希望它这样。大多数情况下,建议使用 'static 生命周期的错误消息是由于试图创建悬垂引用或可用生命周期不匹配而导致的。在这种情况下,解决方案是修复这些问题,而不是指定 'static 生命周期。

泛型类型参数、trait 约束和生命周期一起使用

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

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

这是来自列表 10-21 的 longest 函数,它返回两个字符串切片中较长的一个。但现在它有一个额外的参数,名为 ann,它的泛型类型为 T,该类型可以由任何实现 Display trait 的类型填充,如 where 子句所指定的那样。这个额外的参数将使用 {} 打印,这就是为什么需要 Display trait 约束的原因。因为生命周期是一种泛型,所以生命周期参数 'a 和泛型类型参数 T 的声明都在函数名后面的尖括号内的同一个列表中。

总结

我们在本章中涵盖了很多内容!现在您了解了泛型类型参数、trait 和 trait 约束以及泛型生命周期参数,您就可以编写在许多不同情况下都可以使用的、没有重复的代码。泛型类型参数使您可以将代码应用于不同的类型。Trait 和 trait 约束确保即使类型是泛型的,它们也会具有代码所需的行为。您学习了如何使用生命周期注解来确保这种灵活的代码不会有任何悬垂引用。并且所有这些分析都在编译时发生,不会影响运行时性能!

信不信由你,关于我们在本章中讨论的主题还有很多要学习的:第 17 章讨论了 trait 对象,这是使用 trait 的另一种方法。还有更复杂的涉及生命周期注释的场景,您仅在非常高级的场景中才需要;对于这些场景,您应该阅读 Rust 参考手册。但是接下来,您将学习如何在 Rust 中编写测试,以便您可以确保您的代码按预期工作。