使用 Lifetimes 验证引用
Lifetimes 是我们已经在使用的另一种通用词。生命周期不是确保类型具有我们想要的行为,而是确保引用在我们需要的时候都是有效的。
我们在“参考资料和
Borrowing“ 部分的定义是 Rust 中的每个引用都有一个生命周期,这是该引用有效的范围。大多数时候,生命周期是隐式和推断的,就像大多数时候,类型是推断的一样。只有当有多个类型可用时,我们才必须注释类型。以类似的方式,当引用的生命周期可以以几种不同的方式关联时,我们必须注释生命周期。Rust 要求我们使用通用的生命周期参数来注释关系,以确保运行时使用的实际引用绝对有效。
注释生命周期不是大多数其他编程语言所具有的概念,因此这会让人感到陌生。虽然我们不会在本章中完整地介绍生命周期,但我们将讨论你可能遇到生命周期语法的常见方式,以便你熟悉这个概念。
使用 Lifetimes 防止悬空引用
生存期的主要目的是防止悬空引用,这会导致程序引用它要引用的数据以外的数据。考虑示例 10-16 中的程序,它有一个 outer scope 和一个 internal scope。
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {r}");
}
示例 10-16:尝试使用值已超出范围的引用
注意:示例 10-16、10-17 和 10-23 中的例子声明了变量,但没有给它们初始值,所以变量名存在于外部作用域中。乍一看,这似乎与 Rust 没有 null 值相冲突。但是,如果我们尝试在给变量赋值之前使用变量,我们会得到一个编译时错误,这表明 Rust 确实不允许 null 值。
外部范围声明一个名为 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:r
和
x
,分别命名为 'a
和 'b
在这里,我们用 'a
和 x
的生命周期注释了 r
的生命周期
与 'b
.如您所见,内部的 'b
块比外部的 'b 块小得多
“一
辈子的封禁。在编译时,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 如何分析生命周期以确保引用始终有效,让我们在函数的上下文中探索参数和返回值的通用生命周期。
函数中的通用生命周期
我们将编写一个函数,返回两个字符串切片中较长的 one。此函数将采用两个字符串切片并返回一个字符串切片。在我们实现了 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:调用最长
的
函数来查找两个字符串切片中较长的
请注意,我们希望函数采用字符串切片,即引用,而不是字符串,因为我们不希望最长
的函数获得其参数的所有权。请参阅 “String Slices as
Parameters“部分,以更多地讨论为什么我们在示例 10-19 中使用的参数是我们想要的。
如果我们尝试实现示例 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:最长
的
函数返回两个字符串切片中较长的 (LONGER),但尚未返回
编译
相反,我们得到以下关于生命周期的错误:
$ 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
是 case 还是
else
case 将执行。我们也不知道将要传入的引用的具体生命周期,因此我们无法像在清单 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 多个引用的通用生命周期参数是如何相互关联的。让我们看看 lifetime 注解在 longest
函数的上下文中是如何相互关联的。
函数签名中的 Lifetime Annotations
要在函数签名中使用生命周期注释,我们需要在函数名称和参数列表之间的尖括号内声明泛型生命周期参数,就像我们对泛型类型参数所做的那样。
我们希望签名表达以下约束:只要两个参数都有效,返回的引用就会有效。这是参数的生命周期与返回值之间的关系。我们将生命周期命名为 '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:最长
的函数定义
指定签名中的所有引用必须具有相同的生命周期
'一个
此代码应该编译并生成我们想要的结果,当我们将其与
main
函数。
函数签名现在告诉 Rust,对于某个生命周期 'a
,该函数采用两个参数,这两个参数都是字符串切片,其生命周期至少与生命周期 'a
一样长。函数签名还告诉 Rust 从函数返回的字符串 slice 的寿命至少与 lifetime 'a
一样长。
实际上,这意味着
longest
函数与函数参数引用的值的生命周期中较小的值相同。这些关系是我们希望 Rust 在分析此代码时使用的关系。
请记住,当我们在此函数签名中指定生命周期参数时,我们不会更改传入或返回的任何值的生命周期。相反,我们指定 borrow 检查器应拒绝任何不符合这些约束的值。请注意,longest
函数不需要确切知道 x
和 y
将存活多长时间,只需将满足此签名的 'a
替换为某些范围即可。
在函数中注释生命周期时,注释位于函数签名中,而不是函数体中。生命周期注解成为函数契约的一部分,就像签名中的类型一样。让函数签名包含生命周期契约意味着 Rust 编译器所做的分析可以更简单。如果函数的注释方式或调用方式存在问题,编译器错误可以更准确地指向代码的一部分和约束。相反,如果 Rust 编译器对我们想要的生命周期关系做出更多推断,那么编译器可能只能指出我们的代码的使用与问题的原因相距甚远。
当我们将具体引用传递给 longest
时,替换 'a
的具体生命周期是 x
的范围中与 y
的范围重叠的部分。换句话说,泛型生命周期 'a
将获得等于 x
和 y
生命周期中较小者的具体生命周期。因为我们使用相同的生命周期参数 'a
对返回的引用进行了注释,所以返回的引用在 x
和 y
的生命周期中,以较小者为准。
让我们看看生命周期注解如何通过传入具有不同具体生命周期的引用来限制最长
的函数。示例 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:使用引用具有不同具体生命周期的 String
值的 longest
函数
在此示例中,string1
在外部范围 string2
结束之前有效
在内部范围结束之前有效,而 result
引用的内容在内部范围结束之前有效。运行此代码,您将看到 borrow checker 批准;它将编译并打印 The longest string is long string is long
.
接下来,让我们尝试一个例子,它显示
result
必须是两个参数的较小生命周期。我们将 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}");
}
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
该错误显示 for result
对 println!
语句
string2
需要在外部范围结束之前有效。Rust 知道这一点,因为我们使用相同的生命周期参数 'a
) 注释了函数参数的生命周期并返回了值。
作为人类,我们可以查看这段代码,发现 string1
的长度大于
string2
,因此,result
将包含对 string1
的引用。由于 string1
尚未超出范围,因此对 string1
的引用对 println!
语句仍然有效。但是,在这种情况下,编译器看不到引用是否有效。我们已经告诉 Rust,最长
函数返回的引用的生命周期与传入的引用的生命周期中较小的一个相同。因此,借用检查器不允许示例 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
超出范围,并在最长
函数结束时被清理。我们还尝试返回对 result
的引用
从函数。我们无法指定 lifetime 参数
会更改悬空引用,并且 Rust 不允许我们创建一个悬空
参考。在这种情况下,最好的解决方法是返回拥有的数据类型
而不是引用,因此调用函数负责
清理值。
归根结底,生命周期语法是关于连接各种参数的生命周期和函数的返回值。一旦它们连接起来,Rust 就有足够的信息来允许内存安全的作,并禁止会创建悬空指针或以其他方式违反内存安全的作。
结构定义中的生命周期注释
到目前为止,我们定义的结构体都持有 owned 类型。我们可以定义结构体
来保存引用,但在这种情况下,我们需要添加一个 lifetime 注解
在结构体定义中的每个引用上。示例 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:一个保存引用的结构体,需要一个生命周期注解
此结构具有包含字符串 slice 的单个字段部分
,该 slice 是一个引用。与泛型数据类型一样,我们在结构体名称后的尖括号内声明泛型生命周期参数的名称,以便我们可以在结构体定义的主体中使用生命周期参数。此注释意味着 ImportantExcerpt
的实例不能超过其 part
字段中的引用。
此处的 main
函数创建 ImportantExcerpt
结构的实例,该实例包含对变量 novel
拥有的 String
的第一句话的引用。novel
中的数据存在于 ImportantExcerpt
之前
实例。此外,在 ImportantExcerpt
超出范围之前,novel
不会超出范围,因此
重要Excerpt
实例有效。
终身省略
您已经了解到,每个引用都有一个生命周期,并且您需要为使用引用的函数或结构指定生命周期参数。然而,我们在示例 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
分配给所有 output lifetime 参数。这第三条规则使方法更易于读取和写入,因为需要的符号更少。
让我们假设我们是编译器。我们将应用这些规则来计算示例 10-25 中 first_word
函数签名中引用的生命周期。签名开始时没有任何与引用关联的生命周期:
fn first_word(s: &str) -> &str {
然后,编译器应用第一条规则,该规则指定每个参数都有自己的生命周期。我们像往常一样将其称为 'a
,所以现在签名是这样的:
fn first_word<'a>(s: &'a str) -> &str {
第二条规则适用,因为只有一个 input 生命周期。第二条规则指定将一个输入参数的生命周期分配给输出生命周期,因此签名现在为:
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
。在研究了所有三个规则之后,我们仍然没有弄清楚 return 类型的生命周期是什么。这就是为什么我们在尝试编译示例 10-20 中的代码时遇到错误的原因:编译器通过了生命周期省略规则,但仍然无法弄清楚签名中引用的所有生命周期。
因为第三条规则实际上只适用于方法签名,所以我们接下来将查看该上下文中的生命周期,以了解为什么第三条规则意味着我们不必经常在方法签名中注释生命周期。
方法定义中的 Lifetime 注解
当我们在具有生命周期的结构体上实现方法时,我们使用与示例 10-11 中所示的泛型类型参数相同的语法。我们在何处声明和使用生命周期参数取决于它们是与 struct 字段相关,还是与方法参数和返回值相关。
结构体字段的生命周期名称始终需要在 impl
之后声明
关键字,然后在结构体名称之后使用,因为这些生命周期是
的结构类型。
在 impl
块内的方法签名中,引用可能与结构体字段中引用的生命周期相关联,或者它们可能是独立的。此外,生命周期省略规则通常使得方法签名中不需要生命周期注释。让我们看一些使用示例 10-24 中定义的名为 ImportantExcerpt
的结构体的示例。
首先,我们将使用一个名为 level
的方法,其唯一参数是对
self
的 i32
的 i 值,它不是对任何东西的引用:
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, }; }
有两个 input 生命周期,所以 Rust 应用第一个生命周期省略规则,并给 &self
和 announcement
自己的生命周期。然后,由于其中一个参数是 &self
,因此返回类型将获得 &self
的生命周期,并且所有生命周期都已考虑在内。
静态生命周期
我们需要讨论的一个特殊生命周期是 'static
,它表示受影响的引用可以在程序的整个持续时间内存在。所有字符串 Literals 都有 'static
生命周期,我们可以按如下方式对其进行注释:
#![allow(unused)] fn main() { let s: &'static str = "I have a static lifetime."; }
此字符串的文本直接存储在程序的二进制文件中,该二进制文件始终可用。因此,所有字符串文本的生存期都是 'static
.
您可能会看到在错误消息中使用“static
lifetime.但是在指定 'static
作为引用的生命周期之前,请考虑一下你拥有的引用是否真的在程序的整个生命周期中都存在,以及你是否希望它如此。大多数情况下,一条错误消息表明“静态
生命周期”是由于尝试创建悬空引用或可用生命周期不匹配而导致的。在这种情况下,解决方案是修复这些问题,而不是指定“static
lifetime.
泛型类型参数、trait bounds 和 lifetimes 一起
让我们简单看一下在一个函数中指定泛型类型参数、trait bounds 和 lifetimes 的语法!
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 中最长
的函数,它返回两个字符串切片中较长的那个。但是现在它有一个名为 ann
的额外参数,该参数是泛型类型 T
,该参数可以由实现 Display
的任何类型填充
trait 中指定的 trait
。此额外参数将使用 {}
打印,这就是需要 Display
特征绑定的原因。因为
lifetimes 是一种泛型,即 lifetime 参数
'a
和泛型类型参数 T
位于函数名称后面的尖括号内的同一列表中。
总结
我们在这一章中介绍了很多!现在你已经了解了泛型类型参数、trait 和 trait bounds,以及泛型生命周期参数,你已经准备好编写适用于许多不同情况的无重复代码了。泛型类型参数允许您将代码应用于不同的类型。trait 和 trait bounds 确保即使类型是泛型的,它们也将具有代码所需的行为。您学习了如何使用生命周期注释来确保此灵活代码不会有任何悬空引用。所有这些分析都在编译时进行,这不会影响运行时性能!
信不信由你,关于我们在本章讨论的主题中,还有很多东西需要学习:第 17 章讨论了 trait 对象,这是使用 trait 的另一种方式。还有一些更复杂的场景涉及生命周期注释,您仅在非常高级的场景中才需要;对于这些,您应该阅读 Rust 参考。但接下来,您将学习如何使用 Rust 编写测试,以确保您的代码按预期方式工作。