生命周期(Lifetimes)入门
本教程共 78 篇 · 第 46 篇 · 更新于 2026-08-08 · 约 12 分钟阅读
本节目标:理解生命周期是引用的有效作用域,弄清它如何防止悬垂引用,学会在函数和结构体里手动标注生命周期,并知道编译器何时能自动推断。
生命周期大概是 Rust 里最让新手头大的概念。但别慌——它解决的其实是一件很朴素的事:保证你手里的引用,指向的数据还活着。
1-1 生命周期是什么
生命周期,就是一个引用的有效作用域,从它诞生到它失效的那段范围。大部分时候你不用管它,因为编译器能自动推导——就像类型大部分时候也能自动推导一样。只有当多个引用的关系编译器推不清时,才需要你手动标注。
把类型推导类比过来很好懂:多种类型混在一起时,编译器常要求你标明类型;多个生命周期共存、推导不出关系时,就要求你标明生命周期。
1-2 悬垂引用:生命周期要防的祸
生命周期的核心使命,是避免悬垂引用——也就是引用了已经释放的数据:
{
let r;
{
let x = 5;
r = &x; // r 引用了 x
} // x 在这里被释放
println!("r: {}", r); // r 还指着已消失的 x!
}
r 指向内部块里的 x,可 x 在内部块结尾就被释放了。等外面再打印 r,它引用的东西已经不合法。编译器直接报错:
error[E0597]: `x` does not live long enough
r 活的作用域比 x 大,却引用了活得更短的 x——这就是危险所在。只要反过来,让 x 比 r 活得久,引用就安全。
1-3 借用检查器
Rust 靠一个叫借用检查器的东西,在编译期比对各个引用的生命周期。还是上例,加注释看得更清楚:
{
let r; // ---------+-- 'a
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
println!("r: {}", r); // |
} // ---------+
r 的生命周期是 'a,x 的是 'b,'b 明显比 'a 小。检查器发现:r 想引用一个活得更短的东西,判定有风险,拒绝编译。这就是生命周期在背后默默保护你。
1-4 函数里的生命周期
来看一个经典函数:返回两个字符串切片里较长那个。
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}
逻辑没问题,却编不过:
error[E0106]: missing lifetime specifier
编译器说:返回值是个引用,但签名没说它借用自 x 还是 y。编译器需要知道返回值和参数的关系,才能做借用检查。可这个函数两个分支分别返回 x 和 y,人都不一定说得清返回的是哪个,编译器更不敢猜。这时就得手动标注生命周期。
1-5 生命周期标注语法
生命周期以 ' 开头,通常叫 'a。标注在 & 之后、类型之前:
&i32 // 普通引用
&'a i32 // 带显式生命周期的引用
&'a mut i32 // 带显式生命周期的可变引用
Note关键认知:生命周期标注不改变任何引用的真实作用域。它只是给编译器看的”契约”,告诉编译器各引用之间该怎么比长短。你标
'a活得久,该释放时照样释放,不会真的变长。标注是为了”取悦编译器”,让它在借用时放心。
给 longest 加上标注:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
和泛型一样,先用 <'a> 声明。这里的意思:对某个生命周期 'a,x、y、返回值都至少活 'a 那么久。实际 'a 取 x 和 y 作用域的重合部分——也就是较小的那个。'a 不等于”等于”,而是”大于等于”。
返回值活 'a 久,所以返回值的有效范围不会超过 x、y 中较短的那个。这样编译器就能安心了。
1-6 返回引用只能来自参数或本地
函数若返回引用,它的生命周期来源只有两种:函数参数的生命周期,或函数体内新建引用的生命周期。
后者就是典型的悬垂引用,必然被拒:
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str() // 返回了函数内本地字符串的引用
}
result 在函数结束就被释放,返回的引用却还指着它,编译器直接报错,从源头杜绝悬垂。解决办法是返回所有权:fn longest(_x: &str, _y: &str) -> String,把 String 交出去。
1-7 结构体里的生命周期
结构体字段也能是引用,但每个引用都得标生命周期:
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first = novel.split('.').next().expect("找不到 '.'");
let i = ImportantExcerpt { part: first };
}
<'a> 声明后,part: &'a str 表示:结构体引用的那段字符串,活得必须不短于结构体本身。上例里 novel 和 i 都活到 main 结束,满足要求。part 标了 'a,结构体才允许持有引用——这也解释了为什么之前结构体字段多用 String:引用太麻烦,不如直接拿所有权。
Warning若结构体比它引用的数据活得久,编译必报错。比如把
novel放进内部块、在块外还用i,就是结构体比字符串长寿,引用失效。
1-8 编译器如何自动消除标注
你平时写的 fn first_word(s: &str) -> &str 没标生命周期却能过,靠的是生命周期消除规则。编译器按三条默认规则推断:
- 每个引用参数,自动获得一个独立生命周期:
fn foo(x: &i32)视作fn foo<'a>(x: &'a i32);两个参数就'a、'b各一个。 - 若只有一个输入生命周期,它赋给所有输出生命周期。所以
fn first_word(s: &str) -> &str自动变成fn first_word<'a>(s: &'a str) -> &'a str。 - 若有多个输入生命周期,且其中一个是
&self或&mut self,则&self的生命周期赋给所有输出。这让方法用起来很顺。
回到 longest(x: &str, y: &str) -> &str:规则 1 给 x、y 各一个生命周期;但规则 2 要”只有一个输入”、规则 3 要”有 &self”,都不满足。三条都用不上,编译器推不出返回值该跟谁,于是报错,请你手动标。这就是为什么 longest 必须写 <'a>。
1-9 静态生命周期 ‘static
有个特殊生命周期 'static,表示引用能和整个程序活得一样久。字符串字面量就具有 'static,因为它们被硬编码进二进制文件:
let s: &'static str = "我活得久,因为写死在程序里";
Warning遇到生命周期报错,别图省事给类型强行加
T: 'static。'static能”骗过”编译器,但如果你的引用其实没活那么久,就埋下了隐患。正确思路应先检查:是不是造出了悬垂引用,或生命周期关系本就不一致。
1-10 小结
- 生命周期是引用的有效作用域,用来防止悬垂引用;
- 借用检查器在编译期比对生命周期,保证引用指向的数据还活着;
- 多个引用关系推不清时,用
'a手动标注,告诉编译器它们怎么关联; - 标注不改变真实作用域,只是给编译器的契约;
- 返回引用只能来自参数(或返回所有权),不能来自函数内本地值;
- 结构体含引用字段需标生命周期;编译器用三条规则自动消除常见标注;
'static表示与程序同寿,慎用,别拿它掩盖真实问题。
下一章看三个最常用的标准库 trait:Debug、Display、From。