Skip to content

Day 10: 生命周期

今天是《30天精通Rust语言》系列的第十天,也是基础部分最后一章。我们将学习生命周期(Lifetime)。生命周期是Rust中最独特的概念之一,它确保引用在使用的过程中始终有效,防止悬垂引用等安全问题。

生命周期是Rust编译器用于追踪引用有效性的机制。当我们创建一个引用时,该引用有一个生命周期——它在此期间保持有效。

fn main() {
let r; // 声明一个引用,生命周期开始
{
let x = 5;
r = &x; // r引用x,x的有效期在此作用域内
} // x离开作用域,被drop
println!("r: {}", r); // 编译错误!r引用的数据已不存在
}

这段代码会编译失败,因为Rust检测到 r 引用了已离开作用域的 x。生命周期注解帮助编译器确认引用的有效性。

生命周期注解描述了引用生命周期的关系,不改变引用的有效期。

生命周期注解使用单引号开头的小写标识符:

&'a i32 // 生命周期'a的引用
&'mut arr // 生命周期'arr的可变引用

当函数返回引用时,需要标注其生命周期:

// 不标注生命周期会编译错误
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let s1 = String::from("long string");
let s2 = String::from("xyz");
let result = longest(s1.as_str(), s2.as_str());
println!("The longest string is '{}'", result);
}

<'a> 声明了一个生命周期参数 'a,表明输入引用的生命周期必须至少为 'a',返回引用也具有生命周期 'a'。

结构体中持有引用时,必须标注生命周期:

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 excerpt = ImportantExcerpt {
part: first_sentence,
};
println!("Excerpt: {}", excerpt.part);
}

ImportantExcerpt<'a> 意味着结构体实例不能比它引用的数据存活更久。

Rust团队发现某些模式下生命周期可以由编译器推断,无需手动标注。这些规则称为”生命周期消除规则”(Lifetime Elision Rules)。

规则一:每个引用参数都有独立的生命周期。

// 编译器推断为 fn foo<'a>(&'a i32)
fn foo(x: &i32) -> &i32 { x }

规则二:如果只有一个输入生命周期,自动赋给输出生命周期。

// 编译器推断为 fn foo<'a>(&'a i32) -> &'a i32
fn foo(x: &i32) -> &i32 { x }

规则三:如果方法中有一个 &self 或 &mut self,该生命周期赋给所有输出生命周期。

当编译器无法推断生命周期时,必须手动标注:

// 多个输入生命周期,返回引用与其中一个关联
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// 不能确定返回引用与哪个输入关联
fn first_word<'a>(s: &'a str) -> &'a str {
s.split(' ').next().unwrap_or("")
}

'static 是特殊的生命周期,表示在整个程序运行期间有效:

fn main() {
let s: &'static str = "I have a static lifetime.";
println!("{}", s);
}

字符串字面量默认是 'static 生命周期,因为它们直接存储在程序二进制中,始终可用。

当结构体方法涉及引用时,需要使用生命周期:

struct Quote<'a> {
content: &'a str,
}
impl<'a> Quote<'a> {
fn new(content: &'a str) -> Quote<'a> {
Quote { content }
}
// &self的生命周期自动与返回的&str关联
fn content(&self) -> &str {
self.content
}
fn who(&self) -> &str {
"Unknown"
}
}
fn main() {
let q = Quote::new("The only way to do great work is to love what you do.");
println!("Quote: {}", q.content());
}

当函数有多个引用且它们之间无关时,可以声明多个生命周期:

fn mix<'a, 'b>(x: &'a str, y: &'b str) -> &'b str {
if x.len() > y.len() {
x // 这里返回'a,但函数签名声明返回'b,编译错误
} else {
y
}
}

这个例子编译失败,因为返回类型声明了 'b,但 x 的生命周期是 'a。修正:

fn mix<'a>(x: &'a str, y: &str) -> &'a str {
if x.len() > y.len() {
x
} else {
y // y被提升为'a生命周期
}
}

生命周期与所有权是独立但互补的概念:

  • 所有权:决定值的生命周期和何时被drop。
  • 生命周期:追踪引用有效性的编译时检查。
fn main() {
let mut s = String::from("hello");
let r1 = &s; // r1借用s
let r2 = &s; // r2借用s
println!("{} and {}", r1, r2);
// r1和r2在此后不再使用,可以安全地创建可变引用
let r3 = &mut s; // 可变借用
r3.push_str(", world");
println!("{}", r3);
}

借用规则与生命周期协同工作,确保内存安全。

今天的实践任务:

  1. 编写程序验证悬垂引用,创建一个返回局部变量引用的函数,观察编译错误。
  2. 实现 longest 函数,验证不同生命周期参数的用法。
  3. 创建一个持有引用的结构体 Paragraph<'a>,实现其方法。
  4. 编写函数 first_sentence 提取字符串的第一个句子,添加生命周期注解。
  5. 分析一段代码(可以是标准库代码),识别生命周期省略规则的应用。

问:什么时候需要手动标注生命周期?

答:当函数有多个引用参数且返回引用时,编译器需要知道你返回的引用与哪个输入关联,此时需要手动标注。

问:生命周期会改变引用的行为吗?

答:不会。生命周期完全是编译时概念,不产生任何运行时开销。它只是告诉编译器引用的有效期,以便进行借用检查。

今天我们完成了Rust核心概念的学习:

  • 生命周期追踪引用的有效性,防止悬垂引用。
  • 生命周期注解用 ' 语法声明,如 'a。
  • 函数和结构体中的引用需要正确标注生命周期。
  • Rust的省略规则在简单情况下自动推断生命周期。
  • 静态生命周期 'static 表示整个程序运行期间有效。

恭喜你完成了Rust基础部分的学习!这十天涵盖了Rust最核心的概念:所有权、借用、生命周期。这些概念是Rust独特性的根基,掌握它们将让你能够编写安全、高效的Rust代码。

接下来的二十天,我们将深入更多高级主题,包括泛型、trait、集合、迭代器、并发等。继续保持学习的热情!