Oduah Chigozie 2024-09-06
生命周期是 Rust 中的一项基础机制。在任何具有一定复杂度的 Rust 项目中,你几乎肯定需要处理生命周期。
尽管生命周期对 Rust 项目至关重要,但理解起来却可能相当棘手。因此,我编写了本指南,旨在更清晰地解释生命周期是什么,以及何时应该使用它们。
本教程的先决条件
为了从本教程中获得最大收益,你需要具备以下条件:
- 至少具备 Rust 的初学者水平:本教程不用于学习如何用 Rust 编程,仅用于理解 Rust 中的生命周期及其工作原理。
- 熟悉泛型(Generics):Rust 中的泛型与主流编程语言中的泛型工作方式相同。了解任何语言中泛型的工作原理都会有所帮助。
- 了解借用检查器(borrow checker)的工作原理虽然不像前两点那样是硬性要求,但会很有帮助。反过来,理解生命周期也有助于理解借用检查器的工作机制。
那么,Rust 中的生命周期到底是什么?
为了让 Rust 的借用检查器在整个代码中确保内存安全,它需要知道程序执行期间所有数据的存活时间。在某些情况下,这一点很难做到,而这些情况正是你需要使用显式生命周期注解的地方。
Rust 中的生命周期是一种机制,用于确保代码中发生的所有借用都是有效的。一个变量的生命周期是指它在程序执行过程中存活的时间长度,从初始化开始,到被销毁结束。
借用检查器在许多情况下都能自动检测出变量的生命周期。但在它无法确定的情况下,你就需要通过显式的生命周期注解来协助它。
显式生命周期注解的语法是一个单引号后跟一组用于标识的字符(例如 'static、'a),如下所示:
max<'a>
这个生命周期注解表示 max 的存活时间最多与 'a 一样长。
使用多个生命周期遵循相同的语法:
max<'a, 'b>
在这种情况下,生命周期注解表示 max 的存活时间最多与 'a 和 'b 一样长。
显式生命周期注解的处理方式与泛型类似。让我们看一个例子:
fn max<'a>(s1: &'a str, s2: &'a str) -> &'a str {
// 返回两个字符串中较长的那个
}
在这个例子中,生命周期注解表明 max 的存活时间最多与 s1 或 s2 的生命周期一样长。它还表明 max 返回的引用与 s1 具有相同的生命周期。
Rust 项目中有许多情况需要显式生命周期注解,接下来的几节我们将逐一介绍。
函数中的生命周期注解
只有当函数从其参数中返回一个引用时,才需要显式的生命周期注解。让我们看一个例子:
fn max<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
如果你移除生命周期注解,你会收到 LSP(Language Server Protocol,语言服务器协议)的警告,提示你需要添加生命周期注解。如果你忽略 LSP 的警告并尝试编译代码,你会得到相同的错误信息,但这次是编译器报错。例如:
fn max(s1: &str, s2: &str) -> &str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
输出:
error[E0106]: missing lifetime specifier
--> src/main.rs:44:31
|
44 | fn max(s1: &str, s2: &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 `s1` or `s2`
help: consider introducing a named lifetime parameter
|
44 | fn max<'a>(s1: &'a str, s2: &'a str) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `lifetime-test` (bin "lifetime-test") due to 1 previous error
另一方面,如果函数不从其参数中返回引用,则不需要显式生命周期。例如:
fn print_longest(s1: &str, s2: &str) {
if s1.len() > s2.len() {
println!("{s1} is longer than {s2}")
} else {
println!("{s2} is longer than {s1}")
}
}
同样,如果函数返回的是一个新值(而非引用),也不需要显式生命周期注解:
fn join_strs(s1: &str, s2: &str) -> String {
let mut joint_string = String::from(s1);
joint_string.push_str(s2);
return joint_string;
}
只有当函数从其参数中返回一个借用的引用时,才需要指定生命周期。
结构体(Structs)中的生命周期注解
当结构体的任意字段是引用类型时,就需要显式的生命周期注解。这使得借用检查器能够确保结构体字段中的引用比结构体本身存活时间更长。例如:
struct Strs<'a, 'b> {
x: &'a str,
y: &'b str,
}
如果没有生命周期注解,你会收到与上一节类似的 LSP 和编译器错误信息:
struct OtherStruct {
x: &str,
y: &str,
}
输出:
error[E0106]: missing lifetime specifier
--> src/main.rs:7:8
|
7 | x: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
6 ~ struct OtherStruct<'a> {
7 ~ x: &'a str,
|
error[E0106]: missing lifetime specifier
--> src/main.rs:8:8
|
8 | y: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
6 ~ struct OtherStruct<'a> {
7 | x: &str,
8 ~ y: &'a str,
|
For more information about this error, try `rustc --explain E0106`.
error: could not compile `lifetime-test` (bin "lifetime-test") due to 2 previous errors
方法(Methods)中的生命周期注解
关于方法的生命周期注解,可以应用于独立方法、impl 块或 trait。我们分别来看:
独立方法(Standalone Methods)
为独立方法标注生命周期与为函数标注生命周期完全相同:
impl Struct {
fn max<'a>(self: &Self, s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
}
Impl 块(Impl Blocks)
如果 impl 块所关联的结构体在其定义中包含生命周期注解,那么该 impl 块也需要显式的生命周期注解。语法如下:
struct Struct<'a> {
}
impl<'a> Struct<'a> {
}
这使得你在 impl 块中编写的任何方法都可以返回来自 Struct 的引用。例如:
struct Strs<'a> {
x: &'a str,
y: &'a str,
}
impl<'a> Strs<'a> {
fn max(self: &Self) -> &'a str {
if self.y.len() > self.x.len() {
self.y
} else {
self.x
}
}
}
Trait
trait 中的生命周期注解取决于该 trait 所定义的方法。
我们来看一个例子。如果 trait 中的方法使用了显式生命周期注解(如同独立方法),那么 trait 定义本身不需要显式生命周期注解。例如:
trait Max {
fn longest_str<'a>(s1: &'a str, s2: &'a str) -> &'a str;
}
impl<'a> Max for Struct<'a> {
fn longest_str(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
}
但是,如果 trait 方法需要返回与其关联结构体中的引用,那么 trait 定义本身就需要显式生命周期注解。例如:
trait Max<'a> {
fn max(self: &Self) -> &'a str;
}
其实现方式如下:
struct Strs<'a> {
x: &'a str,
y: &'a str,
}
trait Max<'a> {
fn max(self: &Self) -> &'a str;
}
impl<'a> Max<'a> for Strs<'a> {
fn max(self: &Self) -> &'a str {
if self.y.len() > self.x.len() {
self.y
} else {
self.x
}
}
}
枚举(Enums)中的生命周期注解
与结构体类似,如果枚举的任意变体(variant)包含引用类型的字段,就需要显式生命周期注解。例如:
enum Either<'a> {
Str(String),
Ref(&'a String),
}
'static 生命周期
在许多 Rust 项目中,你可能会遇到具有 'static 生命周期的变量。本节将简要介绍 'static 生命周期是什么、它如何工作,以及常见的使用场景。
'static 是 Rust 中一个保留的生命周期名称。它表示引用所指向的数据从初始化开始一直存活到程序结束。这与静态变量(static variables)略有不同——静态变量直接存储在程序的二进制文件中。不过,所有静态变量都具有 'static 生命周期。
具有 'static 生命周期的变量也可以在运行时创建,但它们不能被 drop(释放),只能被强制转换(coerced)为更短的生命周期。例如:
// 生命周期注解 'a 是两个参数 s1 和 s2 中较短的那个
fn max<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}
fn main() {
let first = "First string"; // 较长的生命周期
{
let second = "Second string"; // 较短的生命周期
// 在 max 函数中,first 的生命周期被强制转换为 second 的生命周期
println!("The biggest of {} and {} is {}", first, second, max(first, second));
};
}
字符串字面量(string literals)就是具有 'static 生命周期的典型例子。它们也存储在程序的二进制文件中,并且可以在运行时使用。
Rust 允许你使用 static 关键字声明静态变量,语法如下:
static IDENTIFIER: &'static str = "value";
静态变量可以在任何作用域中声明,包括全局作用域。这意味着你可以将静态变量用作全局变量。例如:
static FIRST_NAME: &'static str = "John";
static LAST_NAME: &'static str = "Doe";
fn main() {
println!("First name: {}", FIRST_NAME);
println!("Last name: {}", LAST_NAME);
}
静态变量可以是可变的或不可变的。但可变静态变量只能在 unsafe 块中使用,因为它们是不安全的:
static mut FIRST_NAME: &'static str = "John";
static LAST_NAME: &'static str = "Doe";
fn main() {
unsafe {
println!("First name: {}", FIRST_NAME);
}
println!("Last name: {}", LAST_NAME);
unsafe {
FIRST_NAME = "Jane";
println!("First name changed to: {}", FIRST_NAME);
}
}
总结
Rust 中的生命周期帮助借用检查器确保所有借用的引用都是有效的。借用检查器在许多情况下都能自动检测变量的生命周期,但在它无法确定的情况下,你需要通过显式生命周期注解来协助它。
显式生命周期注解就是你在许多 Rust 项目中看到的那些 'a、'b 和 'static。你只需要在处理引用的结构(如结构体、枚举、trait 和 impl 块)以及接收并返回引用的函数或方法中使用它们。
在本指南中,你学习了显式生命周期注解,并看到了一些使用示例。希望它为你带来了更多清晰的理解,帮助你更好地掌握生命周期这一概念。
感谢阅读!