跳到主要内容

生命周期

生命周期(Lifetime)是 Rust 确保引用始终有效的机制。它让编译器能够在编译时检查引用是否有效,避免悬垂引用问题。

什么是生命周期?

问题引入

考虑以下代码:

fn main() {
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+

这段代码存在一个问题:变量 x 在内部作用域结束时被释放,但 r 仍然试图引用它。这就是典型的悬垂引用(Dangling Reference)问题。

Rust 编译器会拒绝这段代码,因为它检测到 r 的生命周期 'ax 的生命周期 'b 更长。当 x 被释放后,r 就指向了无效的内存。

生命周期的概念

在 Rust 中,生命周期是指引用有效的范围。每个引用都有一个生命周期,大多数情况下编译器可以自动推断,但在某些情况下需要我们显式标注。

生命周期的主要目的是:防止悬垂引用。编译器需要确保引用在其指向的数据被释放之前不再使用。

生命周期标注语法

基本语法

生命周期标注使用撇号(')开头,后跟一个简短的名称,通常是小写字母:

&i32        // 普通引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用

生命周期标注不会改变引用的实际生命周期长度,它们只是描述多个引用之间的关系,帮助编译器进行借用检查。

标注位置

生命周期标注可以出现在多个位置:

// 函数签名中的生命周期
fn first<'a>(x: &'a str, y: &'a str) -> &'a str {
// ...
x
}

// 结构体中的生命周期
struct ImportantExcerpt<'a> {
part: &'a str,
}

// impl 块中的生命周期
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}

函数中的生命周期

何时需要生命周期标注

当函数参数和返回值之间存在引用关系,且编译器无法自动推断时,需要显式标注生命周期。

根据 Rust 官方文档,以下是编译器能够自动推断的情况(称为生命周期省略规则):

  1. 每个引用参数都获得一个独立的生命周期参数
  2. 如果只有一个输入生命周期,它被赋给所有输出生命周期
  3. 如果有 &self&mut selfself 的生命周期被赋给所有输出生命周期

基本示例

// 编译器可以自动推断:规则 1 和规则 2 生效
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();

for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}

&s[..]
}

// 需要显式标注:多个输入参数,返回值与参数的关系不明确
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}

详细解释

longest 函数需要生命周期标注的原因:

fn main() {
let string1 = String::from("long string");
let result;

{
let string2 = String::from("xyz");
result = longest(&string1, &string2);
println!("最长的字符串: {}", result); // 正确
}

// 如果这里使用 result,会报错
// println!("{}", result); // 错误!string2 已被释放
}

生命周期 'a 表示返回值的生命周期与参数中较短的那个一致。在上面的例子中,result 的生命周期等于 string2 的生命周期,所以 string2 被释放后,result 也不能再使用。

返回值不总是来自参数

// 返回值只与 x 相关
fn longest_x<'a>(x: &'a str, y: &str) -> &'a str {
x // 只返回 x,所以只需要 x 的生命周期
}

// 错误:不能返回局部变量的引用
// fn invalid<'a>(x: &str, y: &str) -> &'a str {
// let result = String::from("hello");
// &result // result 在函数结束时被释放,不能返回其引用
// }

结构体中的生命周期

当结构体持有引用时,需要在结构体定义中添加生命周期标注:

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, // excerpt 持有的引用不能比 novel 活得更久
};

println!("引用: {}", excerpt.part);
}

这段代码告诉编译器:ImportantExcerpt 实例不能比它持有的引用 part 存活更久。

结构体方法的生命周期

struct ImportantExcerpt<'a> {
part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
// 规则 3 生效:self 的生命周期被赋给返回值
fn level(&self) -> i32 {
3
}

// 返回值生命周期与 self 相同(由规则 3 推断)
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("注意: {}", announcement);
self.part
}

// 比较两个字符串,返回较长的
// 返回值的生命周期与 self.part 相同
fn compare_parts<'b>(&self, other: &'b str) -> &str {
if self.part.len() > other.len() {
self.part
} else {
self.part
}
}
}

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.level());
println!("部分: {}", excerpt.announce_and_return_part("公告内容"));
}

多个生命周期参数

在某些情况下,结构体可能需要多个生命周期参数:

struct Context<'s, 'c> {
text: &'s str,
comment: &'c str,
}

impl<'s, 'c> Context<'s, 'c> {
fn get_text(&self) -> &'s str {
self.text
}

fn get_comment(&self) -> &'c str {
self.comment
}

// 返回值与 text 的生命周期相关
fn analyze(&self) -> &'s str {
println!("评论: {}", self.comment);
self.text
}
}

fn main() {
let text = String::from("主要内容");
{
let comment = String::from("这是一条评论");
let ctx = Context {
text: &text,
comment: &comment,
};
println!("{}", ctx.get_text());
println!("{}", ctx.get_comment());
}
// comment 已被释放,但 text 仍然有效
}

静态生命周期

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

// 字符串字面量具有 'static 生命周期
let s: &'static str = "我有静态生命周期";

fn print_static(s: &'static str) {
println!("{}", s);
}

fn main() {
let s1: &'static str = "hello";
print_static(s1);

// 错误:String 的引用不能自动转换为 &'static str
// let s2 = String::from("world");
// print_static(&s2); // 编译错误
}

'static 的使用场景

  1. 字符串字面量:存储在程序的只读数据段,生命周期与程序相同
  2. 全局常量:使用 static 关键字声明的变量
  3. 需要在多线程间共享的数据
use std::sync::Mutex;

// 全局静态变量
static COUNTER: Mutex<i32> = Mutex::new(0);

fn increment() {
let mut num = COUNTER.lock().unwrap();
*num += 1;
}

注意:不要轻易使用 'static 来"解决"生命周期问题。如果编译器要求某个引用是 'static,通常有更好的解决方案,比如使用 ArcBox 或者让数据活得足够久。

生命周期省略规则

Rust 编译器在某些情况下可以自动推断生命周期,这些规则称为生命周期省略规则:

规则一:每个引用参数获得独立的生命周期

// 编译器看到的:
fn foo<'a>(x: &'a i32) { }
fn bar<'a, 'b>(x: &'a i32, y: &'b i32) { }

规则二:如果只有一个输入生命周期,它被赋给所有输出生命周期

// 编写时:
fn first_word(s: &str) -> &str { }

// 编译器推断为:
fn first_word<'a>(s: &'a str) -> &'a str { }

规则三:如果有 &self 或 &mut self,self 的生命周期被赋给输出

struct ImportantExcerpt<'a> {
part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
// 编写时:
fn announce_and_return_part(&self, announcement: &str) -> &str {
self.part
}

// 编译器推断为:
// fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'a str
}

如果应用这三条规则后,编译器仍然无法确定返回值的生命周期,就会报错,需要显式标注。

生命周期与泛型

生命周期可以与泛型结合使用:

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!("公告: {}", ann);
if x.len() > y.len() { x } else { y }
}

fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";

let result = longest_with_an_announcement(
&string1,
string2,
"今天是个好日子",
);
println!("最长的字符串: {}", result);
}

生命周期子类型与约束

生命周期约束语法

// 'a: 'b 表示 'a 至少和 'b 一样长('a outlives 'b)
fn choose_first<'a, 'b>(first: &'a i32, second: &'b i32) -> &'a i32
where
'a: 'b, // 'a 的生命周期必须比 'b 长或相等
{
first
}

where 子句中的生命周期

fn process<'a, 'b>(x: &'a str, y: &'b str) -> &'a str
where
'a: 'b, // 'a 必须比 'b 活得久
{
println!("y: {}", y);
x
}

实际应用示例

解析器模式

生命周期在实现解析器时非常有用,可以避免不必要的字符串复制:

struct Parser<'a> {
input: &'a str,
position: usize,
}

impl<'a> Parser<'a> {
fn new(input: &'a str) -> Self {
Parser { input, position: 0 }
}

fn peek(&self) -> Option<char> {
self.input[self.position..].chars().next()
}

fn advance(&mut self) -> Option<char> {
let ch = self.peek()?;
self.position += ch.len_utf8();
Some(ch)
}

fn parse_identifier(&mut self) -> &'a str {
let start = self.position;
while let Some(ch) = self.peek() {
if ch.is_alphanumeric() || ch == '_' {
self.advance();
} else {
break;
}
}
&self.input[start..self.position]
}

fn skip_whitespace(&mut self) {
while let Some(ch) = self.peek() {
if ch.is_whitespace() {
self.advance();
} else {
break;
}
}
}
}

fn main() {
let code = "hello_world = 42";
let mut parser = Parser::new(code);

parser.skip_whitespace();
let ident = parser.parse_identifier();
println!("标识符: {}", ident); // 输出: hello_world
}

文本处理

struct TextProcessor<'a> {
text: &'a str,
words: Vec<&'a str>,
}

impl<'a> TextProcessor<'a> {
fn new(text: &'a str) -> Self {
let words: Vec<&str> = text.split_whitespace().collect();
TextProcessor { text, words }
}

fn find_word(&self, word: &str) -> Option<&'a str> {
self.words.iter().find(|&&w| w == word).copied()
}

fn get_word(&self, index: usize) -> Option<&'a str> {
self.words.get(index).copied()
}

fn word_count(&self) -> usize {
self.words.len()
}

fn unique_words(&self) -> Vec<&'a str> {
let mut result = Vec::new();
for &word in &self.words {
if !result.contains(&word) {
result.push(word);
}
}
result
}
}

fn main() {
let text = "Rust 是一门系统编程语言 Rust 很安全";
let processor = TextProcessor::new(text);

println!("总词数: {}", processor.word_count());

if let Some(word) = processor.find_word("Rust") {
println!("找到: {}", word);
}

if let Some(word) = processor.get_word(0) {
println!("第一个词: {}", word);
}

println!("唯一词: {:?}", processor.unique_words());
}

常见错误和解决方案

错误 1:生命周期不匹配

// 错误:无法确定返回值的生命周期
// fn bad_example<'a>(x: &str, y: &str) -> &'a str {
// if x.len() > y.len() { x } else { y }
// }

// 正确:显式标注生命周期
fn good_example<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}

错误 2:返回局部变量的引用

// 错误:返回了局部变量的引用
// fn create_string() -> &str {
// let s = String::from("hello");
// &s // s 在函数结束时被释放
// }

// 正确方案 1:返回所有权
fn create_string_owned() -> String {
String::from("hello")
}

// 正确方案 2:传入可变引用,修改后返回
fn fill_string<'a>(output: &'a mut String) -> &'a str {
*output = String::from("hello");
output
}

错误 3:结构体持有无效引用

struct ImportantExcerpt<'a> {
part: &'a str,
}

// 错误:结构体比引用活得更久
fn bad_struct() {
let excerpt;
{
let text = String::from("hello");
excerpt = ImportantExcerpt { part: &text };
} // text 被释放
// println!("{}", excerpt.part); // 悬垂引用!
}

// 正确:确保引用比结构体活得久
fn good_struct() {
let text = String::from("hello");
let excerpt = ImportantExcerpt { part: &text };
println!("{}", excerpt.part); // 正确
}

错误 4:循环引用中的生命周期

// 注意:以下代码在实际中可能涉及更复杂的生命周期关系
struct Node<'a> {
value: i32,
next: Option<&'a Node<'a>>,
}

fn main() {
let node1 = Node { value: 1, next: None };
let node2 = Node { value: 2, next: Some(&node1) };

println!("node2.next.value: {}", node2.next.unwrap().value);
}

生命周期与智能指针

当需要更灵活的生命周期管理时,可以考虑使用智能指针:

use std::rc::Rc;

// 使用 Rc 来共享所有权,避免复杂的生命周期标注
fn create_shared() -> Rc<String> {
Rc::new(String::from("shared data"))
}

fn main() {
let data = create_shared();
let data_clone = Rc::clone(&data);

println!("data: {}", data);
println!("data_clone: {}", data_clone);
println!("引用计数: {}", Rc::strong_count(&data)); // 2
}

当生命周期关系过于复杂时,使用 RcArcBox 通常是更好的选择:

// 复杂的生命周期关系
struct Complex<'a, 'b> {
data: &'a str,
other: &'b str,
}

// 使用智能指针简化
struct Simple {
data: Rc<String>,
other: Rc<String>,
}

高阶生命周期约束

for<'a> 语法

在某些高级场景中,需要表达"对于任意生命周期都成立"的约束,这时使用 for<'a> 语法:

// 高阶 trait 约束(Higher-Rank Trait Bounds)
fn apply_to_all<F>(f: F)
where
F: for<'a> Fn(&'a str) -> &'a str,
{
let s1 = String::from("hello");
let result = f(&s1);
println!("{}", result);
}

fn main() {
// 这个函数满足 for<'a> Fn(&'a str) -> &'a str
apply_to_all(|s| s);
}

闭包中的生命周期

fn call_with_ref<'a, F>(s: &'a str, f: F) -> usize
where
F: Fn(&'a str) -> usize,
{
f(s)
}

fn main() {
let s = String::from("hello");
let len = call_with_ref(&s, |s| s.len());
println!("长度: {}", len);
}

常见模式速查

模式 1:返回引用

// 返回值与输入的生命周期相同
fn find<'a>(haystack: &'a str, needle: &str) -> Option<&'a str> {
haystack.find(needle).map(|i| &haystack[i..])
}

模式 2:结构体持有引用

struct View<'a> {
data: &'a [u8],
start: usize,
end: usize,
}

impl<'a> View<'a> {
fn as_slice(&self) -> &'a [u8] {
&self.data[self.start..self.end]
}
}

模式 3:借用 self 的方法

impl<'a> Parser<'a> {
// 返回值与 self 的生命周期相同
fn parse_next(&mut self) -> Option<&'a str> {
// 实现...
None
}
}

模式 4:将生命周期参数绑定到 trait

trait HasLifetime<'a> {
fn get(&self) -> &'a str;
}

struct Holder<'a> {
data: &'a str,
}

impl<'a> HasLifetime<'a> for Holder<'a> {
fn get(&self) -> &'a str {
self.data
}
}

小结

本章我们学习了 Rust 生命周期的核心概念:

  1. 生命周期的目的:防止悬垂引用,确保引用始终有效
  2. 生命周期标注语法'a 语法,描述引用之间的关系
  3. 函数生命周期:当编译器无法推断时需要显式标注
  4. 结构体生命周期:持有引用的结构体需要标注生命周期
  5. 生命周期省略规则:编译器的自动推断规则
  6. 静态生命周期'static 的含义和使用场景
  7. 生命周期约束'a: 'b 语法,表达生命周期之间的关系

理解生命周期是掌握 Rust 的关键。虽然初学时可能感觉复杂,但当你熟悉这些规则后,会发现它们提供了一种强大的方式来保证代码的安全性。

延伸阅读

练习

  1. 编写一个函数,接受两个字符串切片,返回其中较长的那个
  2. 创建一个结构体,存储字符串切片,并实现一个返回部分文本的方法
  3. 解释为什么以下代码无法编译:
    fn main() {
    let r;
    {
    let x = 5;
    r = &x;
    }
    println!("{}", r);
    }
  4. 实现一个简单的文本解析器,使用生命周期避免不必要的字符串复制
  5. 编写一个函数,接受一个字符串切片的向量,返回最长的切片