使用生命周期验证引用
生命周期是我们一直在使用的另一种泛型。生命周期的作用不是确保类型具有我们需要的行为,而是确保引用在我们需要的范围内有效。
我们在第四章“引用与借用”一节中没有讨论的一个细节是,Rust 中的每个引用都有一个*生命周期*,即该引用有效的范围。大多数情况下,生命周期是隐式的,就像大多数情况下类型是推断出来的一样。只有当可能存在多种类型时,我们才必须标注类型。类似地,当引用的生命周期可能以几种不同的方式相关时,我们必须标注生命周期。Rust 要求我们使用泛型生命周期参数来标注这些关系,以确保在运行时使用的实际引用一定是有效的。
标注生命周期并不是大多数其他编程语言所具有的概念,因此这会让人感到陌生。虽然我们不会在本章中完整地介绍生命周期,但我们会讨论您可能会遇到的常见生命周期语法,以便您熟悉这个概念。
使用生命周期防止悬空引用
生命周期的主要目的是防止*悬空引用*,这会导致程序引用非预期的数据。考虑代码清单 10-16 中的程序,它有一个外部作用域和一个内部作用域。
fn main() {
let r;
{
let x = 5;
r = &x;
}
println!("r: {r}");
}
注意:代码清单 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}"); // |
} // ---------+
在这里,我们用 '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}"); // | | // --+ | } // ----------+
这里,x
的生命周期为 'b
,在本例中比 'a
大。这意味着 r
可以引用 x
,因为 Rust 知道 r
中的引用在 x
有效时始终有效。
现在您已经了解了引用的生命周期是什么,以及 Rust 如何分析生命周期以确保引用始终有效,让我们在函数的上下文中探索参数和返回值的泛型生命周期。
函数中的泛型生命周期
我们将编写一个函数,该函数返回两个字符串切片中较长的一个。此函数将获取两个字符串切片并返回一个字符串切片。在我们实现了 longest
函数之后,清单 10-19 中的代码应该打印 最长的字符串是 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}");
}
请注意,我们希望该函数获取字符串切片(即引用),而不是字符串,因为我们不希望 longest
函数获取其参数的所有权。有关为什么我们在清单 10-19 中使用的参数是我们想要的参数的更多讨论,请参阅第 4 章中的“字符串切片作为参数”部分。第 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
}
}
相反,我们会收到以下有关生命周期的错误:
$ 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 所示。
文件名: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-19 中的 main
函数一起使用时产生我们想要的结果。
函数签名现在告诉 Rust,对于某个生命周期 'a
,该函数接受两个参数,这两个参数都是至少与生命周期 'a
一样长的字符串切片。函数签名还告诉 Rust,从函数返回的字符串切片将至少与生命周期 'a
一样长。实际上,这意味着 longest
函数返回的引用的生命周期与函数参数引用的值的较短生命周期相同。这些关系是我们希望 Rust 在分析此代码时使用的。
请记住,当我们在 этом function signature, we’re not changing the lifetimes of any values passed in or returned. Rather, we’re specifying that the borrow checker should reject any values that don’t adhere to these constraints. Note that the longest
function doesn’t need to know exactly how long x
and y
will live, only that some scope can be substituted for 'a
that will satisfy this signature.
在函数中注释生命周期时,注释位于函数签名中,而不是函数体中。生命周期注释成为函数契约的一部分,就像签名中的类型一样。让函数签名包含生命周期契约意味着 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}"); } } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
在这个例子中,string1
在外部作用域结束之前一直有效,string2
在内部作用域结束之前一直有效,而 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
}
}
当我们尝试编译此代码时,会收到以下错误:
$ 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
尚未超出作用域,所以对 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().expect("Could not find a '.'"); let i = ImportantExcerpt { part: first_sentence, }; }
此结构体具有单个字段 part
,该字段持有一个字符串切片,它是一个引用。与泛型数据类型一样,我们在结构体名称后面的尖括号内声明泛型生命周期参数的名称,以便我们可以在结构体定义的主体中使用生命周期参数。此注解意味着 ImportantExcerpt
的实例不能比其 part
字段中持有的引用存活时间更长。
这里的 main
函数创建了一个 ImportantExcerpt
结构体的实例,该实例持有对变量 novel
拥有的 String
的第一句话的引用。novel
中的数据在创建 ImportantExcerpt
实例之前就存在。此外,novel
在 ImportantExcerpt
超出作用域之后才超出作用域,因此 ImportantExcerpt
实例中的引用是有效的。
生命周期省略
您已经了解到每个引用都有一个生命周期,并且您需要为使用引用的函数或结构体指定生命周期参数。但是,在第 4 章中,我们在清单 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); }
此函数在没有生命周期注解的情况下编译的原因是历史原因:在 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().expect("Could not find a '.'"); 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().expect("Could not find a '.'"); let i = ImportantExcerpt { part: first_sentence, }; }
有两个输入生命周期,因此 Rust 应用第一条生命周期省略规则,并为 &self
和 announcement
提供它们自己的生命周期。然后,因为其中一个参数是 &self
,所以返回类型获得了 &self
的生命周期,并且所有生命周期都已计算在内。
静态生命周期
我们需要讨论的一个特殊生命周期是 'static
,它表示受影响的引用可以存在于程序的整个生命周期中。所有字符串字面量都具有 'static
生命周期,我们可以按如下方式对其进行注释
#![allow(unused)] fn main() { let s: &'static str = "I have a static lifetime."; }
此字符串的文本直接存储在程序的二进制文件中,该文件始终可用。因此,所有字符串字面量的生命周期都是 'static
。
您可能会在错误消息中看到使用 'static
生命周期的建议。但是在将 'static
指定为引用的生命周期之前,请考虑您拥有的引用是否真的存在于程序的整个生命周期中,以及您是否希望它如此。大多数情况下,建议使用 'static
生命周期的错误消息是由于尝试创建悬空引用或可用生命周期不匹配造成的。在这种情况下,解决方案是解决这些问题,而不是指定 'static
生命周期。
泛型类型参数、特征边界和生命周期的结合
让我们简要地看一下在一个函数中指定泛型类型参数、特征边界和生命周期的语法!
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
特征的类型填充,如 where
子句所指定。这个额外的参数将使用 {}
打印,这就是需要 Display
特征边界的原因。因为生命周期是一种泛型,所以生命周期参数 'a
和泛型类型参数 T
的声明位于函数名称后面的尖括号内的同一个列表中。
总结
我们在本章中涵盖了很多内容!现在您已经了解了泛型类型参数、特征和特征边界以及泛型生命周期参数,您就可以编写没有重复且适用于许多不同情况的代码了。泛型类型参数允许您将代码应用于不同的类型。特征和特征边界确保即使类型是泛型的,它们也将具有代码所需的行为。您学习了如何使用生命周期注释来确保此灵活的代码不会有任何悬空引用。所有这些分析都发生在编译时,不会影响运行时性能!
信不信由你,我们在本章中讨论的主题还有很多要学习的内容:第 17 章讨论了特征对象,这是使用特征的另一种方式。还有一些涉及生命周期注释的更复杂的情况,您只需要在非常高级的情况下才会用到;对于这些情况,您应该阅读Rust 参考。但是接下来,您将学习如何在 Rust 中编写测试,以便确保您的代码按预期工作。