4.4 生命周期(lifetimes)
生命周期是 Rust 语言中一个核心且独特的概念。它确保了所有引用都是有效的,从而避免了悬垂指针(dangling references)这类内存安全问题。虽然初学时可能会觉得生命周期有些复杂,但它的核心思想非常直观:生命周期描述了引用在多长时间内是有效的。
为什么需要生命周期?
当我们编写代码时,一个引用必须始终指向一个有效的内存地址。如果引用的目标被释放了,而引用还在使用,就会导致悬垂指针。Rust 编译器在编译时通过生命周期注解来检查这种问题。
考虑以下代码,它试图返回一个局部变量的引用:
fn main() {
let r;
{
let x = 5;
r = &x; // 尝试将 r 指向 x
} // x 在这里被销毁
println!("r: {}", r); // r 指向了已经被释放的内存
}
这段代码无法通过编译。编译器会报错说 x 的生命周期不够长。x 的生命周期仅限于它所在的内部作用域,而 r 试图在外部作用域使用它。生命周期注解就是用来明确这种关系的工具。
生命周期注解语法
生命周期注解不会改变引用的实际存活时间,它只是让 Rust 编译器能够理解不同引用之间的关系。注解通常以撇号 ' 开头,后面跟一个名称(通常是小写),例如 'a、'b。它被放在引用符号 & 之后,并用空格与类型隔开:
&i32 // 一个普通的引用
&'a i32 // 一个具有生命周期 'a 的引用
&'a mut i32 // 一个具有生命周期 'a 的可变引用
函数中的生命周期注解
当函数接受多个引用参数并返回一个引用时,我们需要告诉编译器返回值的生命周期与哪个参数的生命周期相关。看下面的例子,它试图返回两个字符串切片中较长的一个:
// 这个函数定义缺少生命周期注解,无法编译
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
编译器无法推断返回的引用是来自 x 还是 y,因此不知道它的生命周期应该与谁关联。我们需要添加泛型生命周期参数来明确这种关系:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
这个注解 <'a> 表示:对于某个生命周期 'a,函数接受两个具有该生命周期的字符串切片,并返回一个同样具有该生命周期的字符串切片。这意味着返回的引用在 x 和 y 中较短的那个有效期内都是有效的。
生命周期省略规则
为了让代码更简洁,Rust 编译器内置了一些自动推断生命周期的规则,称为“生命周期省略规则”。在以下情况下,你可以省略生命周期注解:
- 每个引用参数都有自己的生命周期。
- 如果只有一个输入生命周期参数,那么它会被赋予给所有输出生命周期参数。
- 如果有多个输入生命周期参数,但其中一个是
&self或&mut self(方法),那么self的生命周期会被赋予给所有输出生命周期参数。
例如,以下函数无需显式注解:
fn first_word(s: &str) -> &str {
// ...
}
编译器会自动将其推断为 fn first_word<'a>(s: &'a str) -> &'a str。
结构体中的生命周期注解
当结构体需要持有引用时,必须在结构体定义中为这些引用添加生命周期注解:
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,
};
}
这里,ImportantExcerpt 结构体包含一个引用 part,其生命周期必须与结构体实例的生命周期相关联。注解 <'a> 确保了结构体实例不会比它引用的数据存活得更久。
静态生命周期 'static
'static 是一个特殊的生命周期,它表示引用在整个程序运行期间都是有效的。所有字符串字面量(例如 "hello")都具有 'static 生命周期。
let s: &'static str = "I have a static lifetime.";
虽然 'static 很强大,但应谨慎使用。通常,当你看到错误提示“需要 'static 生命周期”时,更合理的做法是检查是否真的需要引用在整个程序期间都有效,而不是直接使用 'static。
总结
生命周期是 Rust 中保证内存安全的关键机制。它通过编译时检查,确保所有引用都指向有效数据。理解生命周期注解的语法和省略规则,以及如何在函数和结构体中使用它们,是掌握 Rust 所有权系统的重要一步。虽然一开始可能感觉有些约束,但一旦习惯,你会发现它极大地提高了代码的可靠性和安全性。
