概述
生命周期(lifetimes)是 Rust 初学者最容易卡住的主题之一,因为它不像“变量”“函数”“结构体”那样直观。
先记住这句最重要的话:
生命周期不是用来延长引用寿命的,而是用来描述引用之间关系的。
这篇文章只讲生命周期本身,目标是把以下问题讲清楚:
- 为什么 Rust 需要生命周期
- 借用检查器在防什么问题
- 为什么
longest这种函数需要生命周期注解 - 生命周期省略规则到底帮你省掉了什么
'static是什么,它为什么经常被误用
核心问题
Rust 允许你借用值,但不允许你拿着一个已经失效的引用继续用。
例如下面这段代码:
// 这些代码不能编译!
{
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
问题在于:
x在内部作用域结束时就被销毁了r却还想在外层作用域里继续引用它
如果 Rust 允许这段代码通过,r 就会变成悬垂引用。
这正是生命周期要防的问题。
借用检查器到底在检查什么
Rust 编译器里有一个很重要的机制:借用检查器(borrow checker)。
它不会“运行程序看看会不会出错”,而是在编译阶段检查:
- 这个引用借用了谁
- 被借用的值会活多久
- 引用会活多久
- 引用的有效范围是否越界
如果引用活得比被引用值还久,编译器就会拒绝。
用作用域图理解
无效示例:
// 这些代码不能编译!
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
这里:
r的作用域是'ax的作用域是'b
而 'b 明显比 'a 短,所以 r 不能引用 x。
有效示例:
{
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {}", r); // | |
// --+ |
} // ----------+
这次 x 活得比 r 久,所以引用有效。
最小示例:为什么 longest 不能直接写
很多人第一次真正接触生命周期,是在这个函数:
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
}
}
这句到底在说什么
它不是说:
- “把
x和y都延长到'a”
而是在说:
x至少在'a这段时间内有效y至少在'a这段时间内有效- 返回值也只保证在
'a这段时间内有效
所以真正的含义是:
返回值的有效期不会超过两个输入引用重叠的那段时间。
这就是为什么生命周期注解是在“描述关系”,而不是“操控寿命”。
有效与无效的调用
有效示例
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);
}
}
这里 result 的使用没有超出 string2 的作用域,所以是安全的。
无效示例
// 这些代码不能编译!
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);
}
这里的问题是:
result可能引用的是string2- 但
string2已经在内部作用域结束时被销毁了
所以编译器必须拒绝。
返回值必须和输入引用有关联
这是生命周期最重要的规则之一。
看这个例子:
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
这里可以工作,因为返回值明确来自 x,所以只需要和 x 建立关系。
再看这个错误例子:
// 这些代码不能编译!
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}
为什么不行?
因为:
result是函数内部创建的局部值- 函数结束时它就被销毁了
- 你却想返回一个指向它的引用
所以当函数想返回引用时,返回值通常必须和某个输入引用建立关系;否则就很容易产生悬垂引用。
什么时候该改成返回拥有所有权的值
如果函数内部创建了新数据,并且要把它交给调用者,通常就不该返回引用,而应该返回拥有所有权的值:
fn make_string() -> String {
String::from("really long string")
}
结构体中的生命周期
结构体里如果存引用,也需要标注生命周期:
struct ImportantExcerpt<'a> {
part: &'a str,
}
这表示:
ImportantExcerpt里保存了一个引用- 这个结构体实例不能活得比
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,
};
}
只要 novel 还活着,first_sentence 和 i.part 就有效。
生命周期省略是什么
很多地方你没写生命周期,代码也能编译。这不是因为生命周期不存在,而是因为编译器按照一组规则帮你补出来了。
这组规则叫生命周期省略(lifetime elision)。
先看一个能省略的例子
fn first_word(s: &str) -> &str {
// ...
}
它之所以能省略,是因为这里只有一个输入引用参数,编译器可以明确判断返回值和它关联。
省略规则最重要的直觉
可以先只记这两条:
- 每个输入引用参数,编译器先假设它有自己的生命周期
- 如果只有一个输入引用,那返回值通常就和它关联
为什么 longest 不能省略
因为它有两个输入引用:
fn longest(x: &str, y: &str) -> &str
这时编译器无法自动判断返回值究竟跟谁绑定,所以你必须手写生命周期关系。
方法里的生命周期为什么常常不用写
看这个方法:
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
这里不需要手写 &self 的生命周期,是因为省略规则已经能推导。
再看一个返回引用的方法:
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
为什么也能省略?
因为方法有一个特殊规则:
- 如果参数里有
&self或&mut self - 返回值默认优先和
self的生命周期关联
所以这里编译器可以理解返回值来自 self.part。
'static 是什么
'static 表示这个引用在程序整个运行期间都有效。
最常见的例子是字符串字面量:
let s: &'static str = "I have a static lifetime.";
因为字符串字面量直接存放在程序二进制里,所以它们天然可以活到程序结束。
为什么初学者容易误用 'static
很多人一看到生命周期报错,就想“那我能不能直接加 'static”。
大多数时候不应该这么干。
因为真正的问题通常不是“生命周期不够长”,而是:
- 你返回了不该返回的引用
- 或者你没有正确表达引用之间的关系
所以 'static 不是修复生命周期报错的万能胶带。
常见误区
误区一:生命周期会延长变量寿命
不会。
生命周期注解只是在描述约束,不会改变值实际活多久。
误区二:生命周期只和函数有关
不是。
结构体里存引用时,同样要处理生命周期。
误区三:只要加上 'static 就能解决问题
大多数时候这是错的。真正应该解决的是引用来源和作用域关系。
总结
| 概念 | 说明 |
|---|---|
| 生命周期 | 描述引用之间的有效关系 |
| 借用检查器 | 在编译期检查借用是否安全 |
| 生命周期注解 | 不延长寿命,只表达关系 |
longest<'a> |
典型的“返回引用必须和输入相关联”的例子 |
| 结构体生命周期 | 结构体保存引用时,也必须描述关系 |
| 生命周期省略 | 编译器在常见场景下自动补全生命周期 |
'static |
表示整个程序期间都有效的引用 |
最佳实践:先问“返回的引用到底来自谁”,再决定生命周期怎么写。不要一上来就背语法。
知识自测
尝试独立回答以下问题,答案在下方。
第 1 题:生命周期的核心作用是什么?
第 2 题:为什么说生命周期不是用来“延长引用寿命”的?
第 3 题:借用检查器在检查什么?
第 4 题:为什么 fn longest(x: &str, y: &str) -> &str 不能直接通过编译?
第 5 题:fn longest<'a>(x: &'a str, y: &'a str) -> &'a str 这句最关键的含义是什么?
第 6 题:为什么函数返回引用时,返回值通常必须和输入引用有关联?
第 7 题:结构体里保存引用时,为什么也要写生命周期?
第 8 题:为什么 first_word 这种函数常常不需要显式生命周期?
第 9 题:'static 是什么?为什么不能拿它到处修生命周期报错?
📖 点击查看答案
第 1 题答案
生命周期的核心作用是描述引用之间的有效关系,并帮助编译器在编译期保证借用安全。
第 2 题答案
因为生命周期注解不会改变值实际存在多久,它只是在函数签名或类型定义里表达约束关系。
第 3 题答案
借用检查器会检查引用是否在其被引用值仍然有效的范围内使用,以及借用关系是否安全。
第 4 题答案
因为编译器不知道返回值到底和 x 还是 y 的生命周期相关,所以无法判断返回引用是否安全。
第 5 题答案
它表示返回值的有效期不能超过两个输入引用共同有效的那段时间。
第 6 题答案
因为如果返回的引用和任何输入都无关,就很可能指向函数内部临时创建、函数结束后就失效的数据。
第 7 题答案
因为结构体实例可能比其中引用的数据活得更久,生命周期注解用来防止这种不安全情况。
第 8 题答案
因为生命周期省略规则在常见简单场景下可以自动推导,不需要手写。
第 9 题答案
'static 表示引用在整个程序期间都有效。它只适用于确实满足这一条件的数据,不能拿来掩盖普通生命周期设计错误。
后续阅读
参考资料
- The Rust Programming Language - Validating References with Lifetimes
- The Rust Programming Language - Advanced Lifetimes