Updated src/generic_types_traits_and_lifetimes/lifetimes.md'.

This commit is contained in:
Hector PENG
2026-03-24 14:34:56 +08:00
parent 562dd6d0db
commit 37040074c7
3 changed files with 58 additions and 43 deletions

View File

@@ -0,0 +1,6 @@
[package]
name = "generic_type_n_trait_bound_n_lifetime"
version = "0.1.0"
edition = "2024"
[dependencies]

View File

@@ -0,0 +1,13 @@
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! ("通知!{}", ann);
if x.len() > y.len() { x } else { y }
}

View File

@@ -358,16 +358,17 @@ fn main() {
**清单 10-24**:存储引用的结构体,需要生命周期注解
结构体拥有保存着是个引用的字符串切片的单一字段 `part`。与通用数据类型泛型generic data types一样,们在结构名字后的尖括号里,声明了通用声明周期参数,进而就可以在结构体定义代码体中使用那个生命周期参数。这个注解表示,`ImportantExcerpt` 的实例,无法存活超过其在 `part` 字段中保存的那个引用this annotation means an instance of `ImportedExcerpt` can't outlive the reference it holds in its `part` field
这个结构体有单个字段,保存着一个字符串切片,而这是个引用。与泛型数据类型一样,们在结构名字后的尖括号内声明泛型生命周期参数的名字,以便我们可以在结构体定义的主体中使用生命周期参数。这个注解意味着 `ImportantExcerpt` 的实例不能存活超过其在 `part` 字段中保存的引用
这里的 `main` 函数创建 `ImportantExcerpt` 结构体的,保存着到变量 `novel` 拥有的 `String` 的第一个句子引用的一个示例`novel` 中的数据在这个 `ImportantExcerpt` 实例创建之前就存在。此外,在这个 `ImportantExcerpt` 超出作用域之前,`novel` 不会超出作用域,因此这个 `ImportantExcerpt` 实例中的引用是有效的。
这里的 `main` 函数创建了一个 `ImportantExcerpt` 结构体的实例,保存着到变量 `novel` 拥有的 `String` 的第一个句子引用。`novel` 中的数据在 `ImportantExcerpt` 实例创建之前就存在。此外,在这个 `ImportantExcerpt` 超出作用域之前,`novel` 不会超出作用域,因此这个 `ImportantExcerpt` 实例中的引用是有效的。
## 生命周期的省略
咱们已经了解到每个引用都有生命周期,以及需要给使用了引用的函数与结构体,指明生命周期参数。不过,在第 4 章中的清单 4-9 中,咱们曾有一个不带生命周期注解也被编译了的函数,下面清单 10-25 中再次予以展示
咱们已经了解到每个引用都有生命周期,并且咱们需要为用到引用的函数与结构体指定生命周期参数。然而,我们在 [清单 4-9](../ownership/the_slice_type.md#listing_4-9) 中曾有函数,下面再次于清单 10-25 中给出,在不带生命周期注解下也编译了
<a name="listing_10-25"></a>
文件名:`src/main.rs`
```rust
@@ -384,79 +385,78 @@ fn first_word(s: &str) -> &str {
}
```
*清单 10-25咱们在清单 4-9 中定义的一个即使其中的参数与返回值均为引用变量,而不带生命周期注解还仍编译了的函数*
**清单 10-25**:咱们在清单 4-9 中定义的一个函数,即使参数与返回值均为引用,也在不带生命周期注解编译了
这个函数不带生命周期注解仍会编译的原因,是历史遗留的:在 Rust 早期版本(`pre-1.0`)中,由于每个引用都需要显式生命周期,因此该代码就不会编译。那个时候,该函数签名会被写成下面这样:
这个函数在不带生命周期注解下就会编译的原因,是历史性的:在 Rust 的早期版本(`pre-1.0`)中,这段代码不会被编译,因为每个引用都需要显式的生命周期。那时,该函数签名将被写成下面这样:
```rust
fn first_word<'a>(s: &'a str) -> &'a str {
```
在编写许多 Rust 代码后Rust 团队发现Rust 程序员们在某些特定情形下,会一次又一次地敲入许多同样的生命周期注解。这些特定情形,是可预测的,并遵循了少数几种确定性模式a few deterministic patterns。Rust 开发者们便把这些模式,编程进了编译器的代码,于是借用检查器可以推断这些情形下的生命周期,而无需显式注解。
在编写大量 Rust 代码后Rust 团队发现 Rust 程序员们在某些特定情形下一次又一次地输入同样的生命周期注解。这些情况是可预测的,并遵循了少数几种确定性模式开发者们编成了这些模式编译器的代码中,以便借用检查器可以推断这些情形下的生命周期而不再需要显式注解。
由于存在合并更多确定性模式,并将他们到编译器的可能,因此讲 Rust 的这段历史是有必要的。今后,或许就只要求更少甚至没有生命周期注解。
Rust 的这段历史是相关的,因为更多的确定性模式可能出现并被添加到编译器。今后,所需的生命周期注解可能将更少
编程到 Rust 引用分析中的那些确定性模式,被称为 *生命周期省略规则lifetime elision rules*。这些规则并非 Rust 程序员遵循的;他们是编译器将考虑的一套特殊情形,并在咱们的代码符合这些情形时,咱们就不需要显式地写生命周期。
编程到 Rust 引用分析中的模式称为 *生命周期省略规则lifetime elision rules*。这些不是要 Rust 程序员遵循的规则;他们是编译器将考虑的一套特殊情形,咱们的代码符合这些情形时,咱们就无需显式地写生命周期。
这些省略规则不提供完全的推断。 Rust 明确地应用这些规则,但仍存在引用具有什么样的生命周期方面的模糊性时,编译器将不会其余引用变量应有什么样的生命周期,加以猜测。相比于猜测,编译器将给咱们,可通过添加生命周期注解解决的一个报错。
省略规则不提供完全的推断。 Rust 应用这些规则,引用具有的生命周期仍然存在歧义时,编译器将不会猜测其余引用变量应如何。编译器不会猜测,而是将给咱们一个报错,咱们可以通过添加生命周期注解解决报错。
函数或方法参数的生命周期,被称为 *输入生命周期input lifetimes*而在返回值的生命周期,则被称为 *输出生命周期output lifetimes*
函数或方法参数的生命周期称为 *输入生命周期input lifetimes*,返回值的生命周期称为 *输出生命周期output lifetimes*
没有显式注解时,编译器会运用三条规则来计算引用的生命周期。条规则适用于输入生命周期,第二及第三条规则,则都适用于输出生命周期。编译器到这三条规则的结束处,仍有其未能计算出生命周期的引用,那么编译器就会以报错停止。这三条规则适用于 `fn` 定义及 `impl` 代码块都适用
没有显式注解时,编译器使用三条规则来计算引用的生命周期。第一条规则适用于输入生命周期,第二及第三条规则适用于输出生命周期。编译器到这三条规则的结束处并且仍然存在无法计算出生命周期的引用时,则编译器将以一个报错停止。这三条规则适用于 `fn` 定义`impl` 代码块。
条规则即为,编译器会指派生命周期参数给是引用的各个参数。也就是说,有着一个参数的函数会获得一个生命周期参数:`fn foo<'a>(x: &'a i32)`而有着两个参数的函数,就会得到两个单独生命周期参数:`fn foo<'a, 'b>(x: &'a i32, &'b i32)`如此等等
第一条规则编译器会给每个是引用的参数指派一个生命周期参数。换句话说,带有一个参数的函数会获得一个生命周期参数:`fn foo<'a>(x: &'a i32)`带有两个参数的函数会得到两个单独生命周期参数:`fn foo<'a, 'b>(x: &'a i32, &'b i32)`以此类推
第二条规则,则是当确切地只有一个输入生命周期参数时,那个生命周期,就被指派给全部输出生命周期参数:`fn foo<'a>(x: &'a i32) -> &'a i32`
第二条规则是,当只有一个输入生命周期参数时,则该生命周期被指派给所有输出生命周期参数:`fn foo<'a>(x: &'a i32) -> &'a i32`
第三条规则,当多个输入生命周期参数,而由于这是个方法(这条规则是针对结构体上的方法),其中之一 `&self``&mut self` 时,那么 `self` 的生命周期,便会被指派给全部输出生命周期参数。由于只有较少必要符号,因此这第三条规则,就会令到方法阅读与编写起来体验更佳
第三条规则,当存在多个输入生命周期参数,但因为这是个方法其中之一 `&self``&mut self` 时, `self` 的生命周期会被指派给所有输出生命周期参数。这条规则使方法更易于读写,因为所需的符号更少
下面咱们就来充当编译器。们将应用这些规则来计算清单 10-25 中`first_word` 函数签名里各个引用的生命周期。函数签名以不带与其中引用关联的任何生命周期开始:
我们来充当编译器。们将应用这些规则来计算 [清单 10-25](#listing_10-25) `first_word` 函数签名引用的生命周期。函数签名以不带与引用关联的任何生命周期开始:
```rust
fn first_word(s: &str) -> &str {
```
随后编译器便应用条规则,其指出了各个参数会获取到各自的生命周期。与平常一样,咱们将把该生命周期叫做 `'a`那么现在函数签名是这样的:
然后,编译器应用第一条规则,其规定每个参数都有自己的生命周期。我们将与往常一样将其称为 `'a`因此现在函数签名是这样的:
```rust
fn first_word<'a>(s: &'a str) -> &str {
```
由于这里只有一个输入生命周期,因此第二条规则便会适用。第二条规则指出,一个输入参数生命周期会被指派给输出生命周期,那么现在这个函数签名就是这样
由于只有一个输入生命周期,因此第二条规则适用。第二条规则规定,一个输入参数生命周期会被指派给输出生命周期,因此该函数签名现在为下面这样:
```rust
fn first_word<'a>(s: &'a str) -> &'a str {
```
现在这个函数签名中的全部引用都有了生命周期,进而编译器可以无需程序员注解函数签名中的生命周期的情况下,继续其分析了
现在这个函数签名中的所有引用都有了生命周期,编译器可以继续分析,而无需程序员注解这个函数签名中的生命周期。
接下来就要看看另一个示例,这次使用清单 10-20 中,一开始编写没有生命周期参数的那个 `longest` 函数
我们来看另一个示例,这次使用 `longest` 函数,当我们在 [清单 10-20](#listing_10-20) 中开始编写他时还没有生命周期参数:
```rust
fn longest(x: &str, y: &str) -> &str {
```
首先来应用第一条规则:个参数都得到自己的生命周期。这次不是一个而是两个参数,那么这里就有两个生命周期:
让我们应用第一条规则:个参数都得到自己的生命周期。这次我们有两个参数而不是一个,因此我们有两个生命周期:
```rust
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
```
咱们可以看出,由于有多于一个的输入生命周期,因此第二条规则便不适用因为 `longest` 是个函数而非方法,参数中没有一个`self`,因此第三条规则也不适用。在历经全部三条规则后,们仍未计算出返回类型的生命周期为何。这就是们在尝试编译清单 10-20 中代码时,收到错误的原因:编译器历经这些生命周期省略规则,而仍未计算出函数签名中引用的全部生命周期。
咱们可以看到,第二条规则不适用,因为有多个输入生命周期。第三条规则不适用因为 `longest` 是个函数而非方法,因此两个参数都不`self`。在逐一检查所有三条规则后,们仍未计算出返回类型的生命周期。这就是为什么我们在尝试编译清单 10-20 中代码时遇到报错:编译器逐一检查了生命周期省略规则,但仍无法计算出签名中引用的所有生命周期。
由于第三条规则实际上只适用于方法签名,咱们接下来就要看看在方法上下文中的生命周期,以发现为何第三条规则,就意味着们不必经常注解方法签名中生命周期。
因为第三条规则实际上只适用于方法签名,所以接下来我们将在这一背景下探讨生命周期,以了解为何第三条规则意味着们不必经常方法签名中注解生命周期。
## 方法定义方面
们在结构体上实现带有生命周期的方法时,们会使用与清单 10-11 中展示的泛型参数同样语法。其中咱们会根据其是否与结构体字段,或方法参数及返回值相关,而声明出并用到生命周期参数
们在带有生命周期的结构体上实现方法时,们会用到与 [清单 10-11](./generics.md#listing_10-11) 中展示的泛型类型参数的相同语法。我们于何处声明及使用生命周期参数,取决于他们是否与结构体字段,或方法参数及返回值相关。
由于结构体字段的生命周期是结构体类型的一部分,因此他们总是需要声明 `impl` 关键字之后,且随后会在结构体名字之后用到
结构体字段的生命周期名字始终需要声明 `impl` 关键字之后,然后在结构体名字后使用,因为这些生命周期属于是结构体类型的一部分
`impl` 代码块的方法签名中,一些引用可能与结构体字段中的引用生命周期相关,也可能他们是独立的。此外,生命周期省略规则通常会发挥作用,从而在方法签名中生命周期注解就不是必要的。咱们来看一些使用咱们曾于清单 10-24 中定义的名为 `ImportantExcerpt` 结构体的示例。
`impl` 代码块的方法签名中,引用可能与结构体字段中的引用生命周期绑定,也可能是独立的。此外,生命周期省略规则通常使方法签名中无需生命周期注解。我们来看一些使用我们在 [清单 10-24](#listing_10-24) 中定义的名为 `ImportantExcerpt` 结构体的示例。
首先,咱们将使用一个名为 `level` 的方法,其唯一参数是`self` 引用,且返回值为非到任何东西引用的 `i32`
首先,咱们将使用一个名为 `level` 的方法,其唯一参数是到 `self` 引用,且返回值为 `i32`,不是到任何内容的引用
```rust
impl<'a> ImportantExcerpt<'a> {
@@ -467,9 +467,9 @@ impl<'a> ImportantExcerpt<'a> {
```
`impl` 后的生命周期参数声明,与类型名字后其使用,都是必的,但由于第一条省略规则的原因,咱们未被要求注解其中`self` 引用的生命周期。
`impl` 后的生命周期参数声明及其在类型名字之后的使用是必的,但由于第一条省略规则,我们无需注解`self` 引用的生命周期。
下面是个其中第三条生命周期省略规则适用的示例:
下面是个第三条生命周期省略规则适用的示例:
```rust
impl<'a> ImportantExcerpt<'a> {
@@ -480,25 +480,25 @@ impl<'a> ImportantExcerpt<'a> {
}
```
这里有两个输入生命周期,那么 Rust 就会适用首条生命周期省略规则,赋予 `&self``announcement` 各自的生命周期。后,由于其中一个参数`&self`那么返回类型就会得到 `&self` 的生命周期,进而全部生命周期都得到了计算
由于有两个输入生命周期Rust 会应用第一条生命周期省略规则,并同时赋予 `&self``announcement` 各自的生命周期。后,因为参数之一`&self`所以返回类型得到 `&self` 的生命周期,而所有生命周期都已计算在内
## 静态生命周期
们需要讨论的一种特殊生命周期是 `'static`表示受其影响的引用*可以* 在程序整个持续时间内存活。所有字符串字面值都有着 `'static` 的生命周期,咱们可将其注解为下面这样
们需要讨论的一种特殊生命周期是 `'static`表示受其影响的引用 *可以* 在程序整个持续时间内存活。所有字符串字面值都有着 `'static` 的生命周期,咱们可以如下注解这种生命周期
```rust
let s: &'static str = "我有静态的生命周期。";
```
字符串的文本,被直接存储在该程序二进制数据中,而这是一直可用的。此,所有字符串字面值的生命周期便`'static`
这个字符串的文本直接存储在该程序二进制数据中,而程序的二进制数据是始终可用的。此,所有字符串字面值的生命周期`'static`
在一些错误消息中,咱们或许会看到使用 `'static` 生命周期的建议。不过在给引用指定 `'static` 生命周期之前,考虑一下手头的这个引用是否会存活到整个程序的生命周期,以及咱们是否想要他存活到整个程序的生命周期。多数时候,建议 `'static` 生命周期的错误消息,都是由尝试创建悬空引用,或可用生命周期不匹配导致。在这情况下,解决法是修复这些问题,而指定 `'static` 生命周期。
咱们可能会在错误消息中看到使用 `'static` 生命周期的建议。但在为引用指定 `'static` 生命周期之前,考虑引用是否确实会存活程序的整个生命周期,以及咱们是否想要他如此。大多数时候,建议 `'static` 生命周期的错误消息,都是由尝试创建悬空引用,或可用生命周期不匹配导致。在这情况下,解决法是修复这些问题,而不是指定 `'static` 生命周期。
# 泛型类型参数、特质边界与生命周期
# 泛型类型参数、特质边界与生命周期三位一体
咱们来简要地看,在一个函数中,一起指定出泛型参数、特质边界与生命周期的语法!
咱们来简要地看一下,在一个函数中同时指定泛型类型参数、特质边界与生命周期的语法!
```rust
use std::fmt::Display;
@@ -512,22 +512,18 @@ where
T: Display,
{
println! ("通知!{}", ann);
if x.len() > y.len() {
x
} else {
y
}
if x.len() > y.len() { x } else { y }
}
```
便是清单 10-21 中,返回两个字符串切片中较长者的 `longest` 函数。不过现在他有了个泛型 `T` 名为 `ann`额外参数,泛型 `T` 可以实现了由 `where` 子句指定的 `Display` 特质的任何类型填入。这个额外参数,将被使用 `{}` 打印出来,这便是为何 `Display` 特质必要的原因。由生命周期是泛型的一种,因此其中的生命周期参数 `'a` 与泛型参数 `T`,便处于函数名后尖括号内的同一清单里
这是 [清单 10-21](#listing_10-21) 中的 `longest` 函数,返回两个字符串切片中较长的那个。但现在他有个名为 `ann` 的泛型类`T` 的额外参数,可以由任何实现 `where` 子句指定的 `Display` 特质的类型填入。这个额外参数使用 `{}` 打印出来,这是为何 `Display` 特质边界是必要的。由生命周期属于泛型类型,所以生命周期参数 `'a` 与泛型类型参数 `T` 的声明位于函数名字之后尖括号内的同一列表中
# 本章小结
在这一章中,咱们谈到了很多东西!现在咱们清楚了泛型参数、特质与特质边界,泛型生命周期参数,那么就准备好编写在许多不同情况下工作,不带有重复的代码。泛型参数实现了将代码应用于不同类型。特质与特质边界确保即使类型是通用的,他们仍将有着代码所需的行为。咱们了解了如何使用生命周期注解,来保证这种灵活代码不会有任何悬空引用。所有分析都发生在编译时,不会影响运行时性能!
我们在这一章中介绍了很多内容!现在咱们已经了解了泛型类型参数、特质与特质边界,以及泛型生命周期参数,咱们已准备好编写没有重复且适用于许多不同情况的代码。泛型类型参数允许咱们应用代码于不同类型。特质与特质边界确保即使类型是泛型,他们仍将有着代码所需的行为。我们学习了如何使用生命周期注解来确保这种灵活代码不会有任何悬空引用。所有这些分析都发生在编译时,不会影响运行时性能!
论相信与否,关于们在本章讨论的这些话题,要掌握的东西远不止这些:第 17 章会讨论特质对象trait objects其是运用特质的另一种方式。还有更多咱们只会在极复杂场景下,才需要的涉及到更复杂场景的生命周期注解;要了解那些生命周期注解,咱们应阅读 [Rust 指南](https://doc.rust-lang.org/reference/index.html)。不过接下来,咱们将了解怎样编写 Rust 中的测试,从而就可以确保咱们的代码以其应有的方式工作。
管咱们信不信,关于们在本章讨论的主题,还有很多内容要学习:第 18 章会讨论特质对象trait objects这是使用特质的另一种方式。还有一些更复杂场景下涉及生命周期注解;对于这些,咱们应阅读 [Rust 指南](https://doc.rust-lang.org/reference/index.html)。接下来,咱们将学习怎样编写 Rust 中的测试,以便确保咱们的代码以其应有的方式工作。
End