生命周期
生命周期(Lifetime)就是引用的有效作用域。大多数时候无需手动声明,编译器会自动推导;但当存在多个引用、编译器无法推断其约束关系时,就需要手动标注。这和类型推导的逻辑完全类似:
- 编译器大多数时候可以自动推导类型 → 同样,编译器大多数时候也可以自动推导生命周期
- 当多种类型同时出现时,编译器要求手动标注类型 → 当多个生命周期存在且编译器无法推导时,就需要手动标注生命周期
生命周期的核心目的只有一个: 防止悬垂引用(Dangling Reference)——即防止程序使用一个已经被释放的内存地址。
悬垂引用与借用检查
看一个经典的悬垂引用例子:
fn main() {
let r;
{
let x = 5;
r = &x; // r 引用了 x
} // x 在这里被释放
println!("{}", r); // ❌ r 现在引用了无效内存
}编译器会拒绝这段代码,报告 x 活得不够久。原因是 Rust 内部有一个借用检查器(Borrow Checker),通过比较引用与被引用值的生命周期来验证所有引用的合法性:
fn main() {
let r; // ------+-- 'a r 的生命周期
// |
{ // |
let x = 5; // -+--'b| x 的生命周期
r = &x; // | |
} // -+ | 'b 在这里结束,x 被释放
// |
println!("{}", r); // | r 还在('a),但引用的 'b 已结束
} // ------+借用检查器发现: r 的生命周期 'a 比它引用的 x 的生命周期 'b 更长('b ⊂ 'a),引用一个比自己早结束的值,就是悬垂引用。
要修复这个问题,只需让被引用的值活得比引用更久:
fn main() {
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a | 'a ⊆ 'b,安全!
println!("{}", r); // | |
// --+ |
} // ----------+生命周期标注语法
生命周期标注并不改变任何引用的实际作用域,它只是给编译器提供信息,描述多个引用之间的约束关系。标注只是让编译器知道"这几个引用应该活得一样久",而不会真正延长或缩短任何值的寿命。
标注语法: 以 ' 开头,名称通常是单个小写字母,'a 最为常用。对于引用类型,生命周期标注位于 & 之后:
&i32 // 普通引用
&'a i32 // 带显式生命周期的引用
&'a mut i32 // 带显式生命周期的可变引用单独一个生命周期标注没有意义,它的价值在于描述多个引用之间的关系,例如:
fn useless<'a>(first: &'a i32, second: &'a i32) {}
// 表示: first 和 second 至少都活得和 'a 一样久函数中的生命周期标注
考虑一个返回两个字符串切片中较长者的函数:
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}这段代码无法编译,错误提示:
error[E0106]: missing lifetime specifier
= help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `x` or `y`编译器在函数签名处无法判断返回值借用自 x 还是 y,也就无法验证调用方对返回值的使用是否安全。解决方法是手动标注生命周期:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}这里的 'a 告诉编译器: x、y 和返回值至少都活得和 'a 一样久。
注意,'a 并不代表"生命周期等于 'a",而是"大于等于 'a"。调用时,'a 的实际大小等于 x 和 y 生命周期中较小的那个,返回值的生命周期也随之等于两者中较短的:
fn main() {
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
// 'a 等于 string2 的生命周期(两者中较短的)
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {}", result); // ✅ result 在 string2 的作用域内使用
}
}若将 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!("{}", result); // ❌ string2 已释放,'a 要求 result 不能活得比 string2 久
}只标注相关参数
生命周期标注只需覆盖与返回值有关的参数。如果函数永远只返回第一个参数,y 与返回值没有关系,就不需要为 y 标注:
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}返回值的生命周期来源
函数返回引用时,生命周期只有两个来源:
- 来自函数参数 — 安全,通过生命周期标注告知编译器
- 来自函数体内新创建的值 — 这是悬垂引用,函数结束时该值被释放
// ❌ 返回了函数体内临时创建的值的引用
fn bad<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str() // result 在函数结束时释放,引用随即失效
}遇到这种情况,正确做法是返回所有权而非引用,把值转移给调用方:
fn good(_x: &str, _y: &str) -> String {
String::from("really long string") // 把所有权转移给调用方
}结构体中的生命周期
结构体字段如果包含引用类型,必须为每个引用字段标注生命周期。语义是: 结构体实例的生命周期不能超过其引用字段所指向的数据:
struct ImportantExcerpt<'a> {
part: &'a str, // 'a 确保 ImportantExcerpt 不会比 part 引用的数据活得更久
}
fn main() {
let novel = String::from("I am George...");
let first_sentence = novel.split('.').next().expect("Could not find '.'");
let i = ImportantExcerpt { part: first_sentence }; // ✅ novel 比 i 活得久
}反过来,结构体比其引用的数据活得更久则编译失败:
fn main() {
let i;
{
let novel = String::from("I am George...");
let first_sentence = novel.split('.').next().unwrap();
i = ImportantExcerpt { part: first_sentence };
} // novel 在这里释放
println!("{:?}", i); // ❌ i 还在用,但 part 引用的数据已释放
}生命周期省略规则
Rust 编译器内置了三条生命周期省略规则(Lifetime Elision Rules),规则能推导出所有引用的生命周期时无需手动标注,规则不适用时则报错要求手动标注。
| 规则 | 适用范围 | 内容 |
|---|---|---|
| 规则一 | 输入生命周期 | 每个引用参数各自获得一个独立的生命周期参数 |
| 规则二 | 输出生命周期 | 若只有一个输入生命周期,则赋给所有输出生命周期 |
| 规则三 | 输出生命周期 | 方法中有 &self 或 &mut self,则 self 的生命周期赋给所有输出 |
记忆口诀: ① 每参一个 ② 独苗传全局 ③ Self 大过天
规则应用示例
示例一: 单参数函数,规则一 + 规则二足以推导完整,无需手动标注
手写 fn first_word(s: &str) -> &str
规则一 fn first_word<'a>(s: &'a str) -> &str
规则二 fn first_word<'a>(s: &'a str) -> &'a str ✅ 推导完成示例二: 两参数函数,三条规则均无法推导返回值,必须手动标注
手写 fn longest(x: &str, y: &str) -> &str
规则一 fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str
规则二 不适用(有两个输入生命周期)
规则三 不适用(不是方法,无 &self)
→ 编译器无法确定返回值的生命周期,报错方法中的生命周期
为含有生命周期的结构体实现方法时,需要在 impl 后声明生命周期参数,语法与泛型一致:
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 { 3 } // 返回值不是引用,无需标注
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
// 规则三: 返回值生命周期自动等于 &self 的生命周期,编译通过
}
}若需要返回值的生命周期与 announcement 参数一致(而不是 self),编译器无法自动推导,需要手动标注并添加生命周期约束:
// 'a: 'b 表示 'a 至少活得和 'b 一样久
impl<'a: 'b, 'b> ImportantExcerpt<'a> {
fn announce_and_return_part(&'a self, announcement: &'b str) -> &'b str {
println!("Attention please: {}", announcement);
self.part
}
}也可以用 where 子句将约束分离出来,可读性更好:
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'b str
where
'a: 'b,
{
println!("Attention please: {}", announcement);
self.part
}
}
'a: 'b是生命周期约束语法,与泛型约束T: Trait完全类似,读作"'a至少活得和'b一样久"。约束的含义是:&'a self是被引用方,所以引用它的&'b str必须活得更短,即'b≤'a,写作'a: 'b。
静态生命周期('static)
'static 是 Rust 中最特殊的生命周期,拥有它的引用能与整个程序共存亡。所有字符串字面量都具有 'static 生命周期,因为它们被编译进二进制文件:
let s: &'static str = "我活得和程序一样久";遇到生命周期编译错误时,不要以为加上
'static就万事大吉。应先思考: 是否存在悬垂引用?是否存在生命周期不匹配? 只有确认引用确实需要活得那么久时,才使用'static。滥用'static可能掩盖真正的内存安全问题。
综合示例: 泛型 + Trait Bound + 生命周期
三者可以组合使用,在同一个函数签名中共存:
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 }
}额外补充
枚举中的生命周期
// 语义: Status 的生命周期不能超过它包含的字符串切片的生命周期
enum Status<'a> {
Success,
Error(&'a str), // 错误信息是借用的,不拥有所有权
}Trait 实现中的生命周期
// 语义: ImportantExcerpt 的生命周期不能超过它包含的字符串切片的生命周期
struct ImportantExcerpt<'a> {
part: &'a str,
}
// 语义: ImportantExcerpt 实现了 Display Trait,且实现中使用了生命周期 'a
impl<'a> Display for ImportantExcerpt<'a> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "{}", self.part)
}
}方法返回值与结构体生命周期解耦
结构体方法默认通过规则三将返回值生命周期绑定到 &self。但如果你明确知道返回的引用来自结构体字段(而非外部参数),可以直接标注字段的生命周期,让返回值的寿命超过方法调用的实例寿命:
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
// 返回 'a 而不是 self 的生命周期
fn get_part(&self) -> &'a str {
self.part
}
}
fn main() {
let data = String::from("很长寿的数据");
let part_ref;
{
let excerpt = ImportantExcerpt { part: &data };
part_ref = excerpt.get_part(); // 拿到标注为 'a(data 的寿命)的引用
} // excerpt 在这里被销毁,但 part_ref 引用的是 data,仍然有效
println!("{}", part_ref); // ✅ 安全!引用的数据(data)还活着
}生命周期子类型化(Subtyping)
'a: 'b 不只是约束语法,也是 Rust 生命周期子类型化的体现: 如果 'a 的作用域完全覆盖 'b,那么在需要 'b 的地方,可以安全地使用 'a(长的可以当短的用,反之不行)。
符号: 'a: 'b
读作: 'a 至少活得和 'b 一样久('a 是 'b 的子类型)
逻辑: 若 'a 的作用域完全覆盖 'b,则 'a 可以安全地当成 'b 来使用