Rust:使用生命周期验证引用

生命周期是我们已经使用的另一种泛型。生命周期不是确保类型具有我们想要的行为,而是确保引用在我们需要的时间内有效。

Rust 中的每个引用都有一个生命周期,即该引用有效的范围。大多数情况下,生命周期是隐式的和推断的,就像大多数情况下类型是推断的一样。只有当有多种类型可能时,我们才必须注释类型。类似地,当引用的生命周期可以以几种不同的方式关联时,我们必须注释生命周期。Rust 要求我们使用通用生命周期参数注释关系,以确保运行时使用的实际引用肯定有效。

生命周期标注是给引用检查器做安全检查的。

使用生命周期防止悬垂引用

1
2
3
4
5
6
7
8
9
10
fn main() {
let r;

{
let x = 5;
r = &x;
}

println!("r: {r}");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ 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

借用检查器

错误

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {r}"); // |
} // ---------+

正确

这里,x 的生命周期为 ‘b,在本例中大于 ‘a。这意味着 r 可以引用 x,因为 Rust 知道只要 x 有效,r 中的引用就始终有效。

1
2
3
4
5
6
7
8
fn main() {
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {r}"); // | |
// --+ |
} // ----------+

函数中的泛型生命周期

1
2
3
4
5
6
7
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ 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

生命周期注解语法

生命周期注解不会改变任何引用的生存期。相反,它们描述了多个引用的生命周期之间的关系,而不会影响生命周期。就像函数在签名指定泛型类型参数时可以接受任何类型一样,函数也可以通过指定泛型生命周期参数来接受具有任何生命周期的引用。

1
2
3
&i32        // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime

函数签名中的生命周期注解

我们希望签名能够表达以下约束:只要两个参数都有效,返回的引用就有效。这是参数和返回值的生命周期之间的关系。我们将生命周期命名为 'a,然后将其添加到每个引用中,如示例所示:

1
2
3
4
5
6
7
8
// x,y 和返回的生命周期一样
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

正例

1
2
3
4
5
6
7
8
9
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}");
}
}

反例

1
2
3
4
5
6
7
8
9
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}");
}

在此例中,因为借用检查器知道的是你们的生命周期一样长,那么在y(string2)的生命周期结束后,x和返回值的生命周期也应该结束了,在引用的生命周期结束后还使用无效引用就会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ 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

从生命周期的角度思考

归根结底,生命周期语法就是将各种参数和函数返回值的生命周期连接起来。一旦连接起来,Rust 就有足够的信息来允许内存安全的操作,并禁止创建悬空指针或以其他方式违反内存安全的操作。

结构体定义中的生命周期注解

到目前为止,我们定义的结构体都保存了从属类型。我们可以定义结构体来保存引用,但在这种情况下,我们需要在结构体定义中的每个引用上添加生命周期注解。示例中有一个名为 ImportantExcerpt 的结构体,它保存了一个字符串切片。

1
2
3
4
5
6
7
8
9
10
11
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,
};
}

此结构体具有单个字段 part,该字段保存字符串切片,即引用。与泛型数据类型一样,我们在结构体名称后的尖括号内声明泛型生命周期参数的名称,以便我们可以在结构体定义的主体中使用生命周期参数。此注释意味着 ImportantExcerpt 的实例不能比其 part 字段中保存的引用存活更久。

生命周期省略规则

Rust 引用分析中编入的模式称为生命周期省略规则。这些不是程序员要遵循的规则;它们是编译器会考虑的一组特殊情况,如果您的代码符合这些情况,您无需明确编写生命周期。

省略规则不提供完整的推理。如果在 Rust 应用规则后,引用的生命周期仍然存在歧义,则编译器不会猜测剩余引用的生命周期应该是多少。编译器不会猜测,而是会给出一个错误,您可以通过添加生命周期注释来解决。

函数或方法参数的生命周期称为输入生命周期,返回值的生命周期称为输出生命周期。

方法定义中的生命周期注解

1
2
3
4
5
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}

静态生命周期

我们需要讨论的一个特殊生命周期是 'static,它表示受影响的引用可以在整个程序运行期间存在。所有字符串文字都具有 'static 生命周期,我们可以对其进行注释如下:

1
let s: &'static str = "I have a static lifetime.";

此字符串的文本直接存储在程序的二进制文件中,该二进制文件始终可用。因此,所有字符串文字的生存期都是“静态的”。

您可能会在错误消息中看到建议使用 ‘static 生命周期。但在将 ‘static 指定为引用的生命周期之前,请考虑您拥有的引用是否真的在程序的整个生命周期中存在,以及您是否希望它存在。大多数情况下,建议使用 ‘static 生命周期的错误消息是由于尝试创建悬空引用或可用生命周期不匹配而导致的。在这种情况下,解决方案是修复这些问题,而不是指定 ‘static 生命周期。

综合示例:泛型类型参数、特征界限和生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
}
}