简介
欢迎来到「使用生命周期验证引用」实验。本实验是 《Rust 程序设计语言》 的一部分。你可以在 LabEx 中练习 Rust 技能。
在本实验中,我们将讨论生命周期以及它们如何确保引用在需要时始终有效。虽然生命周期可能会让人感到陌生,但我们将介绍一些你可能遇到的生命周期语法的常见方式,以帮助你熟悉这个概念。
This tutorial is from open-source community. Access the source code
💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版
欢迎来到「使用生命周期验证引用」实验。本实验是 《Rust 程序设计语言》 的一部分。你可以在 LabEx 中练习 Rust 技能。
在本实验中,我们将讨论生命周期以及它们如何确保引用在需要时始终有效。虽然生命周期可能会让人感到陌生,但我们将介绍一些你可能遇到的生命周期语法的常见方式,以帮助你熟悉这个概念。
生命周期是我们一直在使用的另一种泛型。生命周期不是确保类型具有我们想要的行为,而是确保引用在我们需要的时间内保持有效。
我们在“引用与借用”中没有讨论的一个细节是,Rust 中的每个引用都有一个生命周期,即该引用有效的作用域。大多数情况下,生命周期是隐式的且会被推断出来,就像大多数情况下类型会被推断出来一样。只有在可能存在多种类型时,我们才必须标注类型。类似地,当引用的生命周期可能以几种不同方式相关联时,我们必须标注生命周期。Rust 要求我们使用泛型生命周期参数来标注这些关系,以确保在运行时使用的实际引用肯定是有效的。
标注生命周期甚至不是大多数其他编程语言所具有的概念,所以这会让人感觉陌生。虽然我们不会在本章中全面介绍生命周期,但我们会讨论一些你可能遇到生命周期语法的常见方式,以便你能熟悉这个概念。
生命周期的主要目的是防止悬空引用,悬空引用会导致程序引用的数据并非它原本打算引用的数据。考虑清单 10-16 中的程序,它有一个外层作用域和一个内层作用域。
fn main() {
1 let r;
{
2 let x = 5;
3 r = &x;
4 }
5 println!("r: {r}");
}
清单 10-16:尝试使用其值已超出作用域的引用
注意:清单 10-16、10-17 和 10-23 中的示例声明了变量但未给它们赋初始值,所以变量名存在于外层作用域中。乍一看,这似乎与 Rust 没有空值相冲突。然而,如果我们在给变量赋值之前尝试使用它,会得到一个编译时错误,这表明 Rust 确实不允许空值。
外层作用域声明了一个名为r
的变量,没有初始值[1],内层作用域声明了一个名为x
的变量,初始值为5
[2]。在内层作用域中,我们尝试将r
的值设置为对x
的引用[3]。然后内层作用域结束[4],我们尝试打印r
中的值[5]。这段代码无法编译,因为在我们尝试使用r
时,r
所引用的值已经超出了作用域。以下是错误信息:
error[E0597]: `x` does not live long enough
--> src/main.rs:6:13
|
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
错误信息说变量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
的生命周期注释
在这里,我们用 '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 知道在 x
有效的时候,r
中的引用将始终有效。
既然你已经知道了引用的生命周期在哪里,以及 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:一个 main
函数,调用 longest
函数来找出两个字符串切片中较长的那个
请注意,我们希望函数接受字符串切片(即引用),而不是字符串,因为我们不希望 longest
函数获取其参数的所有权。有关为什么我们在清单 10-19 中使用这些参数的更多讨论,请参考“作为参数的字符串切片”。
如果我们尝试按清单 10-20 所示实现 longest
函数,它将无法编译。
文件名:src/main.rs
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
清单 10-20:longest
函数的实现,返回两个字符串切片中较长的那个,但尚未编译
相反,我们会得到以下关于生命周期的错误:
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 {
| ++++ ++ ++ ++
帮助信息表明返回类型需要一个泛型生命周期参数,因为 Rust 无法判断返回的引用是指向 x
还是 y
。实际上,我们也不知道,因为这个函数体中的 if
块返回对 x
的引用,而 else
块返回对 y
的引用!
当我们定义这个函数时,我们不知道将传递给这个函数的具体值,所以我们不知道 if
情况还是 else
情况会执行。我们也不知道将传入的引用的具体生命周期,所以我们不能像在清单 10-17 和 10-18 中那样查看作用域来确定我们返回的引用是否始终有效。借用检查器也无法确定这一点,因为它不知道 x
和 y
的生命周期与返回值的生命周期有何关系。为了解决这个错误,我们将添加泛型生命周期参数来定义引用之间的关系,以便借用检查器能够进行分析。
生命周期标注并不会改变任何引用的实际存活时长。相反,它们描述的是多个引用的生命周期之间的关系,而不会影响这些生命周期。就像函数在签名中指定泛型类型参数时可以接受任何类型一样,函数通过指定泛型生命周期参数也可以接受具有任何生命周期的引用。
生命周期标注的语法有点特别:生命周期参数的名称必须以单引号('
)开头,并且通常全部小写且非常短,就像泛型类型一样。大多数人会用 'a
作为第一个生命周期标注的名称。我们将生命周期参数标注放在引用的 &
之后,使用空格将标注与引用的类型分隔开。
以下是一些示例:一个没有生命周期参数的 i32
引用、一个具有名为 'a
的生命周期参数的 i32
引用,以及一个同样具有 'a
生命周期的 i32
可变引用。
&i32 // 一个引用
&'a i32 // 一个具有显式生命周期的引用
&'a mut i32 // 一个具有显式生命周期的可变引用
单独一个生命周期标注本身并没有太大意义,因为这些标注的目的是告诉 Rust 多个引用的泛型生命周期参数是如何相互关联的。让我们在 longest
函数的上下文中研究一下生命周期标注是如何相互关联的。
要在函数签名中使用生命周期标注,我们需要在函数名和参数列表之间的尖括号内声明泛型生命周期参数,就像我们对泛型类型参数所做的那样。
我们希望签名表达以下约束:只要两个参数都有效,返回的引用就将有效。这就是参数的生命周期与返回值之间的关系。我们将生命周期命名为 'a
,然后将其添加到每个引用中,如清单 10-21 所示。
文件名:src/main.rs
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 是一个简单的示例。
文件名: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}");
}
}
清单 10-22:对具有不同具体生命周期的 String
值的引用使用 longest
函数
在这个示例中,string1
在外层作用域结束之前都是有效的,string2
在内层作用域结束之前都是有效的,而 result
引用的内容在内层作用域结束之前都是有效的。运行这段代码,你会看到借用检查器通过了;它将编译并打印出 The longest string is long string is long
。
接下来,让我们尝试一个示例,展示 result
中的引用的生命周期必须是两个参数中较小的那个生命周期。我们将把 result
变量的声明移到内层作用域之外,但将其值的赋值留在包含 string2
的作用域内。然后我们将使用 result
的 println!
移到内层作用域结束之后的外层作用域之外。清单 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}");
}
清单 10-23:在 string2
超出作用域后尝试使用 result
当我们尝试编译这段代码时,会得到以下错误:
error[E0597]: `string2` does not live long enough
--> src/main.rs:6:44
|
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
错误表明,为了使 result
对于 println!
语句有效,string2
需要在外层作用域结束之前都有效。Rust 知道这一点,因为我们使用相同的生命周期参数 'a
注释了函数参数和返回值的生命周期。
作为人类,我们可以查看这段代码并看到 string1
比 string2
长,因此,result
将包含对 string1
的引用。因为 string1
尚未超出作用域,所以对 string1
的引用对于 println!
语句仍然有效。然而,编译器在这种情况下无法看出该引用是有效的。我们已经告诉 Rust,longest
函数返回的引用的生命周期与传入的引用的生命周期中较小的那个相同。因此,借用检查器不允许清单 10-23 中的代码,因为它可能有一个无效的引用。
尝试设计更多实验,改变传递给 longest
函数的引用的值和生命周期,以及返回的引用的使用方式。在编译之前,先假设你的实验是否会通过借用检查器;然后检查看看你是否正确!
你需要指定生命周期参数的方式取决于你的函数在做什么。例如,如果我们将 longest
函数的实现改为总是返回第一个参数而不是最长的字符串切片,那么我们就不需要为 y
参数指定生命周期。以下代码将编译通过:
文件名:src/main.rs
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
我们为参数 x
和返回类型指定了生命周期参数 'a
,但没有为参数 y
指定,因为 y
的生命周期与 x
或返回值的生命周期没有任何关系。
当从函数返回一个引用时,返回类型的生命周期参数需要与其中一个参数的生命周期参数相匹配。如果返回的引用不指向其中一个参数,那么它必须指向在这个函数中创建的值。然而,这将是一个悬空引用,因为这个值将在函数结束时超出作用域。考虑一下 longest
函数的这个无法编译的尝试实现:
文件名:src/main.rs
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}
在这里,即使我们为返回类型指定了生命周期参数 'a
,这个实现也会编译失败,因为返回值的生命周期与参数的生命周期完全无关。这是我们得到的错误信息:
error[E0515]: cannot return reference to local variable `result`
--> src/main.rs:11:5
|
11 | result.as_str()
| ^^^^^^^^^^^^^^^ returns a reference to data owned by the
current function
问题在于 result
在 longest
函数结束时超出作用域并被清理。我们还试图从函数中返回对 result
的引用。我们无法指定任何生命周期参数来改变这个悬空引用,并且 Rust 不允许我们创建悬空引用。在这种情况下,最好的解决办法是返回一个拥有所有权的数据类型而不是引用,这样调用函数就负责清理这个值。
最终,生命周期语法是关于连接函数的各种参数和返回值的生命周期。一旦它们连接起来,Rust 就有足够的信息来允许内存安全的操作,并禁止创建悬空指针或以其他方式违反内存安全的操作。
到目前为止,我们定义的结构体都持有拥有所有权的类型。我们可以定义结构体来持有引用,但在这种情况下,我们需要在结构体定义中的每个引用上添加生命周期标注。清单 10-24 中有一个名为 ImportantExcerpt
的结构体,它持有一个字符串切片。
文件名:src/main.rs
1 struct ImportantExcerpt<'a> {
2 part: &'a str,
}
fn main() {
3 let novel = String::from(
"Call me Ishmael. Some years ago..."
);
4 let first_sentence = novel
.split('.')
.next()
.expect("Could not find a '.'");
5 let i = ImportantExcerpt {
part: first_sentence,
};
}
清单 10-24:一个持有引用的结构体,需要生命周期标注
这个结构体有一个名为 part
的字段,它持有一个字符串切片,这是一个引用(第 2 行)。与泛型数据类型一样,我们在结构体名称后的尖括号内声明泛型生命周期参数的名称,以便我们可以在结构体定义的主体中使用生命周期参数(第 1 行)。这个标注意味着 ImportantExcerpt
的实例不能比它在 part
字段中持有的引用存活时间更长。
这里的 main
函数创建了一个 ImportantExcerpt
结构体的实例(第 5 行),该实例持有对变量 novel
(第 3 行)所拥有的 String
的第一句话的引用(第 4 行)。novel
中的数据在 ImportantExcerpt
实例创建之前就已存在。此外,在 ImportantExcerpt
超出作用域之前,novel
不会超出作用域,因此 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[..]
}
清单 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 {
现在这个函数签名中的所有引用都有了生命周期,编译器可以继续进行分析,而无需程序员在这个函数签名中注释生命周期。
让我们看另一个例子,这次使用我们在清单 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
,而不是对任何东西的引用:
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
在 impl
之后的生命周期参数声明以及在类型名称之后对它的使用是必需的,但是由于第一个省略规则,我们不需要注释对 self
的引用的生命周期。
这里有一个第三个生命周期省略规则适用的示例:
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {announcement}");
self.part
}
}
有两个输入生命周期,所以 Rust 应用第一个生命周期省略规则,给 &self
和 announcement
都赋予它们自己的生命周期。然后,因为其中一个参数是 &self
,返回类型获得 &self
的生命周期,并且所有生命周期都已得到解释。
我们需要讨论的一种特殊生命周期是 'static
,它表示受影响的引用可以在程序的整个持续时间内存在。所有字符串字面量都具有 'static
生命周期,我们可以这样标注:
let s: &'static str = "I have a static lifetime.";
这个字符串的文本直接存储在程序的二进制文件中,并且始终可用。因此,所有字符串字面量的生命周期都是 'static
。
你可能会在错误消息中看到使用 'static
生命周期的建议。但是在将 'static
指定为引用的生命周期之前,要考虑你实际拥有的引用是否真的会在程序的整个生命周期内存在,以及你是否希望它如此。大多数情况下,建议使用 'static
生命周期的错误消息是由于试图创建悬空引用或可用生命周期不匹配导致的。在这种情况下,解决方案是修复这些问题,而不是指定 'static
生命周期。
让我们简要看看在一个函数中同时指定泛型类型参数、trait 约束和生命周期的语法!
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
子句指定的任何实现了 Display
trait 的类型填充。这个额外的参数将使用 {}
打印,这就是为什么需要 Display
trait 约束。因为生命周期是一种泛型,所以生命周期参数 'a
和泛型类型参数 T
的声明在函数名后的尖括号内的同一列表中。
恭喜你!你已经完成了“使用生命周期验证引用”实验。你可以在 LabEx 中练习更多实验来提升你的技能。