键盘快捷键

按 ← 或 → 在章节间导航

按 S 或 / 搜索本书

按 ? 显示此帮助

按 Esc 隐藏此帮助

使用生命周期验证引用

生命周期是我们已经使用过的另一种泛型。与确保类型具有所需行为不同,生命周期确保引用在我们需要它们有效的整个期间都保持有效。

第 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“存活时间不够长”。原因是第 7 行内部作用域结束时,x 就会离开作用域。但 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:分别标记为 'a 和 'b 的 r 与 x 的生命周期

这里,我们将 r 的生命周期标注为 'a,将 x 的生命周期标注为 'b。如你所见,内部的 '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 知道只要 x 有效,r 中的引用就始终有效。

现在你已经了解引用的生命周期,以及 Rust 如何分析生命周期来确保引用始终有效,接下来我们将探讨函数参数和返回值中的泛型生命周期。

函数中的泛型生命周期

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

Filename: 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 函数,代码将无法编译。

Filename: 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 那样,通过观察作用域来判断返回的引用是否始终有效。借用检查器也无法判断,因为它不知道 x 和 y 的生命周期与返回值生命周期之间的关系。要修复这个错误,我们会添加泛型生命周期参数来定义引用之间的关系,以便借用检查器进行分析。

生命周期标注语法

生命周期标注不会改变任何引用的存活时长,而是在不影响生命周期的前提下,描述多个引用的生命周期之间的关系。正如函数可以通过在签名中指定泛型类型参数来接受任意类型,函数也可以通过指定泛型生命周期参数来接受具有任意生命周期的引用。

生命周期标注的语法稍显特别:生命周期参数名必须以撇号(')开头,通常像泛型类型一样使用全小写且很短的名称。大多数人会将第一个生命周期标注命名为 'a。我们把生命周期参数标注放在引用的 & 后面,并用空格将标注与引用类型分开。

下面是一些示例:没有生命周期参数的 i32 引用、带有名为 'a 的生命周期参数的 i32 引用,以及同样具有 'a 生命周期的可变 i32 引用:

&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 所示。

Filename: 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 函数不需要确切知道 x 和 y 会存活多久,只需知道存在某个作用域可以替代 'a 并满足该签名即可。

为函数标注生命周期时,标注应放在函数签名中,而不是函数体中。生命周期标注会成为函数契约的一部分,就像签名中的类型一样。让函数签名包含生命周期契约,可以简化 Rust 编译器的分析。如果函数标注方式或调用方式有问题,编译器就能更准确地指出代码中出错的位置和相关约束。反之,如果 Rust 编译器需要更多推断我们期望的生命周期关系,它可能只能指出距离问题根源很远的代码使用处。

向 longest 传入具体引用时,替代 'a 的具体生命周期,是 x 的作用域与 y 的作用域重叠的部分。换句话说,泛型生命周期 'a 会取 x 和 y 生命周期中较短的那个。由于我们为返回引用标注了相同的生命周期参数 'a,因此返回引用也只会在 x 和 y 中较短的生命周期内有效。

接下来,我们传入具有不同具体生命周期的引用,看看生命周期标注如何限制 longest 函数。清单 10-22 是一个简单示例。

Filename: 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 的作用域内为它赋值。然后将使用 result 的 println! 移到内部作用域结束之后。清单 10-23 中的代码无法编译。

Filename: 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 标注了函数参数和返回值的生命周期。

作为人类,我们可以看出 string1 比 string2 长,因此 result 会包含对 string1 的引用。由于 string1 尚未离开作用域,这个引用在 println! 语句处仍然有效。然而,编译器无法看出此处的引用有效。我们已经告诉 Rust,longest 函数返回的引用,其生命周期等于传入引用中较短的那个。因此,借用检查器会拒绝清单 10-23 中的代码,因为它可能包含无效引用。

试着设计更多实验,改变传给 longest 函数的引用值和生命周期,以及返回引用的使用方式。在编译之前,先假设实验能否通过借用检查器,然后再验证自己的判断是否正确!

生命周期关系

生命周期参数的指定方式取决于函数的行为。例如,如果将 longest 函数改为始终返回第一个参数,而不是较长的字符串切片,我们就不需要为 y 参数指定生命周期。下面的代码可以编译:

Filename: 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 函数实现尝试:

Filename: 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、用于保存字符串切片的结构体。

Filename: 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 离开作用域之后才会离开作用域,因此该实例中的引用有效。

生命周期省略

你已经了解到每个引用都有生命周期,并且使用引用的函数或结构体需要指定生命周期参数。不过,我们在清单 4-9 中定义过一个函数,这里在清单 10-25 中再次展示,它没有生命周期标注也能编译。

Filename: 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 之前)的 Rust 版本中,这段代码无法编译,因为每个引用都需要显式生命周期。当时,函数签名必须这样写:

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

编写了大量 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 {

现在,这个函数签名中的所有引用都有了生命周期,编译器可以继续分析,而不需要程序员在签名中显式标注生命周期。

再看另一个例子,这次使用我们在清单 10-20 中开始讨论的 longest 函数;当时它还没有生命周期参数:

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 应用第一条生命周期省略规则,分别为 &self 和 announcement 分配生命周期。接着,由于其中一个参数是 &self,返回类型就获得 &self 的生命周期;至此,所有生命周期都已确定。

'static 生命周期

还需要讨论一种特殊生命周期 '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 的参数。根据 where 子句,T 可以是任何实现了 Display trait 的类型。这个额外参数会通过 {} 打印,因此需要 Display trait 约束。由于生命周期也是一种泛型,生命周期参数 'a 与泛型类型参数 T 的声明会放在函数名后的同一个尖括号列表中。

总结

本章涵盖了很多内容!现在你已经了解泛型类型参数、trait 与 trait 约束,以及泛型生命周期参数,可以开始编写适用于多种情况且没有重复代码的程序了。泛型类型参数让代码可以应用于不同类型;trait 和 trait 约束确保这些泛型类型具有代码所需的行为。你还学会了如何使用生命周期标注,确保灵活的代码不会包含悬垂引用。所有这些分析都在编译时进行,不会影响运行时性能!

信不信由你,本章讨论的主题还有更多内容:第 18 章会介绍 trait 对象,这是使用 trait 的另一种方式。生命周期标注还有一些更复杂的场景,只有在非常进阶的情况下才需要;对此可以阅读Rust 参考。接下来,你将学习如何在 Rust 中编写测试,以确保代码按预期工作。