跳到主要内容

生命周期(Lifetimes)

生命周期(Lifetimes)是 Rust 中另一个与其他编程语言相比独特的功能。生命周期确保引用如预期一直有效。

生命周期避免了悬垂引用

生命周期的主要目标是避免悬垂引用,后者会导致程序引用了非预期引用的数据:

fn main() {
let r;

{
let x = 5;
r = &x; // 错误:`x` 的生命周期不够长
}

// println!("r: {}", r); // 这里会编译错误
}

借用检查器

Rust 编译器有一个借用检查器(borrow checker),它比较作用域来确保所有的借用都是有效的:

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

这里将 r 的生命周期标记为 'a 并将 x 的生命周期标记为 'b。如你所见,内部的 'b 块要比外部的生命周期 'a 小得多。在编译时,Rust 比较这两个生命周期的大小,并发现 r 拥有生命周期 'a,不过它引用了一个拥有生命周期 'b 的对象。

函数中的泛型生命周期

让我们来编写一个返回两个字符串 slice 中较长者的函数:

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

let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}

// 这个函数定义会编译错误
// fn longest(x: &str, y: &str) -> &str {
// if x.len() > y.len() {
// x
// } else {
// y
// }
// }

错误信息揭示了返回值需要一个泛型生命周期参数,因为 Rust 并不知道将要返回的引用是指向 xy

生命周期注解语法

生命周期注解并不改变任何引用的生命周期的长短。相反它们描述了多个引用生命周期相互的关系,而不影响其生命周期。

基本语法规则

生命周期注解有着一个不太常见的语法:

  1. 以撇号开头:生命周期参数名称必须以撇号(')开头
  2. 通常小写:名称通常全是小写,类似于泛型
  3. 名称简短:通常很短,'a'b'c 是常见选择
  4. 描述性名称:也可以使用描述性名称如 'input'output
&i32        // 引用(隐式生命周期)
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用

详细语法示例

1. 函数中的生命周期注解

// 基本语法:在函数名后用尖括号声明生命周期参数
fn function_name<'a>(param: &'a Type) -> &'a ReturnType {
// 函数体
}

// 实际例子
fn get_first<'a>(list: &'a [i32]) -> &'a i32 {
&list[0]
}

// 多个生命周期参数
fn compare<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
if x.len() > y.len() { x } else { x } // 注意:只能返回 'a
}

2. 结构体中的生命周期注解

// 结构体包含引用时需要生命周期注解
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,
};

println!("重要摘录: {}", i.part);
}

3. 方法中的生命周期注解

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!("注意!{}", announcement);
self.part // 返回结构体中的引用
}

// 多个生命周期参数的方法
fn announce_and_return_longest<'b>(
&self,
announcement: &'b str,
) -> &'b str
where
'a: 'b, // 生命周期约束:'a 必须比 'b 活得更久
{
println!("注意!{}", announcement);
if self.part.len() > announcement.len() {
// 这里会编译错误,因为返回类型是 &'b str
// self.part
announcement
} else {
announcement
}
}
}

生命周期注解的位置

1. 函数签名中的位置

// 语法:fn name<'lifetime>(params) -> return_type
fn example<'a, 'b>(
x: &'a str, // 参数1的生命周期
y: &'b str, // 参数2的生命周期
z: &'a mut i32, // 参数3的生命周期(可变引用)
) -> &'a str { // 返回值的生命周期
x
}

2. 结构体定义中的位置

// 语法:struct Name<'lifetime> { fields }
struct Container<'a, 'b> {
first: &'a str, // 字段1的生命周期
second: &'b str, // 字段2的生命周期
number: i32, // 拥有所有权的字段不需要生命周期
}

3. 枚举中的生命周期注解

enum Either<'a, 'b> {
Left(&'a str),
Right(&'b str),
}

fn main() {
let s1 = String::from("hello");
let s2 = String::from("world");

let either = Either::Left(&s1);

match either {
Either::Left(s) => println!("左边: {}", s),
Either::Right(s) => println!("右边: {}", s),
}
}

生命周期参数的命名约定

// 常见的命名约定
fn example1<'a>(x: &'a str) -> &'a str { x } // 单个生命周期用 'a
fn example2<'a, 'b>(x: &'a str, y: &'b str) -> &'a str { x } // 多个用 'a, 'b, 'c...

// 描述性命名(在复杂情况下更清晰)
fn process_input<'input, 'output>(
data: &'input str,
buffer: &'output mut String,
) -> &'output str {
buffer.push_str(data);
buffer
}

// 特殊的静态生命周期
fn get_static_str() -> &'static str {
"这是一个静态字符串" // 存在于整个程序运行期间
}

复杂的生命周期注解示例

// 多个引用参数和返回值
fn longest_with_an_announcement<'a, 'b>(
x: &'a str,
y: &'a str,
ann: &'b str,
) -> &'a str {
println!("公告!{}", ann);
if x.len() > y.len() {
x
} else {
y
}
}

// 结构体方法中的复杂生命周期
struct Parser<'a> {
input: &'a str,
position: usize,
}

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

fn parse_word<'b>(&'b mut self) -> Option<&'a str> {
// 注意:返回的是 &'a str,不是 &'b str
// 因为我们返回的是 input 的一部分,而不是 self 的引用
if self.position >= self.input.len() {
return None;
}

let start = self.position;
while self.position < self.input.len()
&& !self.input.chars().nth(self.position).unwrap().is_whitespace() {
self.position += 1;
}

Some(&self.input[start..self.position])
}
}

fn main() {
let text = String::from("hello world rust");
let mut parser = Parser::new(&text);

while let Some(word) = parser.parse_word() {
println!("解析到单词: {}", word);
parser.position += 1; // 跳过空格
}
}

生命周期注解的关键理解

  1. 不改变生命周期:注解只是告诉编译器引用之间的关系
  2. 编译时检查:帮助编译器验证引用的有效性
  3. 约束关系:描述参数和返回值之间的生命周期关系
  4. 泛型参数:生命周期是一种特殊的泛型参数
// 这个函数说明:
// 1. 参数 x 和 y 都必须至少活到 'a 这么久
// 2. 返回值也会活到 'a 这么久
// 3. 实际的生命周期是 x 和 y 中较短的那个
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 is long");

{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {}", result);
}
}

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

函数签名表明对于某些生命周期 'a,函数会获取两个参数,它们都是与生命周期 'a 存在得至少一样长的字符串 slice。函数会返回一个同样也与生命周期 'a 存在得至少一样长的字符串 slice。

生命周期约束

当我们在函数中使用生命周期注解时,这些注解出现在函数签名中,而不是函数体中的任何代码中:

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!("{}", result); // 这里可以使用 result
}
// println!("{}", result); // 这里会编译错误,因为 string2 已经离开作用域
}

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

深入理解生命周期

指定生命周期参数的正确方式依赖函数实现的具体功能:

// 这个函数总是返回第一个参数,所以只需要为 x 指定生命周期
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}

// 这个函数返回新创建的值,不需要生命周期参数
fn longest_string() -> String {
String::from("really long string")
}

结构体定义中的生命周期注解

目前为止,我们定义的结构体全都持有拥有所有权的类型。也可以定义包含引用的结构体,不过这需要为结构体定义中的每一个引用添加生命周期注解:

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

println!("重要摘录: {}", i.part);
}

这个注解意味着 ImportantExcerpt 的实例不能比其 part 字段中的引用存在得更久。

生命周期省略

在早期版本(pre-1.0)的 Rust 中,这段代码是不能编译的,因为每一个引用都需要明确的生命周期:

fn first_word(s: &str) -> &str { // 实际上编译器推断为 fn first_word<'a>(s: &'a str) -> &'a str
let bytes = s.as_bytes();

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

&s[..]
}

Rust 团队发现在特定情况下 Rust 程序员们总是重复地编写一模一样的生命周期注解。这些场景是可预测的并且遵循几个明确的模式。接着 Rust 团队就把这些模式编程进了 Rust 编译器的代码中,如此借用检查器在这些情况下就能推断出生命周期而不需要作者显式的加上注解。

生命周期省略规则

编译器采用三条规则来判断引用何时不需要明确的注解:

  1. 输入生命周期规则:编译器为每一个引用参数都分配一个生命周期参数
  2. 单一输入生命周期规则:如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数
  3. 方法生命周期规则:如果方法有多个输入生命周期参数并且其中一个参数是 &self&mut self,说明是个对象的方法,那么所有输出生命周期参数被赋予 self 的生命周期

方法定义中的生命周期注解

当为带有生命周期的结构体实现方法时,其语法依然类似泛型类型参数的语法:

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

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

println!("Level: {}", i.level());
let part = i.announce_and_return_part("今天天气不错");
println!("Part: {}", part);
}

静态生命周期

这里有一种特殊的生命周期值得讨论:'static,其生命周期能够存活于整个程序期间:

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

所有的字符串字面值都拥有 'static 生命周期。

结合泛型类型参数、trait bounds 和生命周期

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

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

let result = longest_with_an_announcement(
string1.as_str(),
string2,
"Today is someone's birthday!",
);
println!("The longest string is {}", result);
}

实际应用示例

缓存结构体

struct Cache<'a> {
data: &'a str,
processed: Option<String>,
}

impl<'a> Cache<'a> {
fn new(data: &'a str) -> Self {
Cache {
data,
processed: None,
}
}

fn get_processed(&mut self) -> &str {
if self.processed.is_none() {
self.processed = Some(self.data.to_uppercase());
}
self.processed.as_ref().unwrap()
}
}

fn main() {
let data = "hello world";
let mut cache = Cache::new(data);

println!("Processed: {}", cache.get_processed());
}

最佳实践

  1. 让编译器推断生命周期:大多数情况下不需要显式注解
  2. 理解生命周期的含义:它们描述引用之间的关系,不改变实际生命周期
  3. 在结构体中谨慎使用引用:考虑是否真的需要引用而不是拥有的数据
  4. 使用 'static 要谨慎:确保真的需要整个程序生命周期
// 好的生命周期使用示例
fn find_longest_word(text: &str) -> Option<&str> {
text.split_whitespace()
.max_by_key(|word| word.len())
}

fn main() {
let text = "The quick brown fox jumps over the lazy dog";
if let Some(longest) = find_longest_word(text) {
println!("最长的单词是: {}", longest);
}
}

生命周期是 Rust 确保内存安全的重要机制,虽然概念复杂,但通过实践会逐渐理解其重要性和使用方法。