Rust —— 生命周期

生命周期(Lifetime)是 Rust 中确保引用有效性的机制。每个引用都有生命周期,即引用保持有效的作用域。大多数情况下生命周期是隐式推断的,但在某些情况下需要显式标注。

1. 生命周期的作用

1.1 防止悬垂引用

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

{
let x = 5;
r = &x; // ❌ 错误:x 的生命周期太短
}

println!("r: {}", r);
}

编译器会阻止这段代码,因为 x 在使用 r 之前就被销毁了。

1.2 借用检查器

Rust 的借用检查器比较引用的生命周期,确保所有引用都是有效的。

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

2. 函数中的生命周期标注

2.1 为什么需要标注

1
2
3
4
5
6
7
fn longest(x: &str, y: &str) -> &str {  // ❌ 缺少生命周期标注
if x.len() > y.len() {
x
} else {
y
}
}

编译器无法确定返回的引用是 x 还是 y,因此无法判断返回引用的生命周期。

2.2 生命周期标注语法

1
2
3
&i32        // 引用
&'a i32 // 带显式生命周期的引用
&'a mut i32 // 带显式生命周期的可变引用

生命周期参数名称:

  • ' 开头
  • 通常使用小写字母
  • 'a 是最常用的名称

2.3 函数签名中的生命周期

1
2
3
4
5
6
7
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}

含义: 返回值的生命周期与参数中较短的生命周期相同。

2.4 使用示例

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); // ❌ 错误
}

3. 结构体中的生命周期

3.1 定义包含引用的结构体

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().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}

含义: ImportantExcerpt 实例的生命周期不能超过 part 字段引用的数据。

4. 生命周期省略规则

编译器使用三条规则自动推断生命周期:

  1. 每个引用参数都有自己的生命周期

    1
    2
    3
    fn foo(x: &str) -> &str
    // 等同于
    fn foo<'a>(x: &'a str) -> &'a str
  2. 如果只有一个输入生命周期,赋予所有输出生命周期

    1
    fn first_word(s: &str) -> &str
  3. 如果是方法且有多个输入生命周期,其中之一是 &self&mut self,那么 self 的生命周期赋予所有输出生命周期

4.1 示例

1
2
3
4
5
6
7
8
9
// 不需要标注
fn first_word(s: &str) -> &str {
&s[..1]
}

// 需要标注
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}

5. 方法中的生命周期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct ImportantExcerpt<'a> {
part: &'a str,
}

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

fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}

6. 静态生命周期

'static 表示引用在整个程序期间都有效:

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

使用场景: 字符串字面量、全局变量。

警告: 不要滥用 'static,大多数情况下问题在于尝试创建悬垂引用。

7. 泛型、Trait 和生命周期结合

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
}
}

总结

  • 生命周期标注:描述引用之间的关系,不改变实际生命周期
  • 借用检查器:确保所有引用在使用时都是有效的
  • 省略规则:大多数情况下编译器可以自动推断
  • 结构体生命周期:包含引用的结构体需要标注生命周期
  • ‘static:整个程序期间有效的引用

生命周期是 Rust 确保内存安全的重要机制,虽然初学时可能感到困惑,但理解后能帮助我们编写更安全的代码。

Hooray!生命周期学习完成!!!