生命周期
这一课你将能做到
- 解释生命周期标注的本体:它是对引用「有效范围」的描述,不是延长寿命的开关
- 掌握三条省略规则,判断什么时候必须显式写
'a - 写出带生命周期参数的函数与结构体,读懂 E0106 报错
为什么要学
上一课的借用规则里有一条「引用不能比被引用者活得久」。大多数情况编译器能自己推断,但函数有多个引用参数、或结构体里存引用时,编译器需要你补充标注才能完成验证。
这一课在 Java 工程师视角几乎全新:Java 的引用始终有效(GC 保证对象不被提前回收),根本不存在这个概念。理解它的价值在于——你会真正明白 Rust 为什么敢在无 GC 的前提下宣称「引用永远安全」。
先建立图像
生命周期不是新东西,它一直存在,标注只是给编译器看的「范围说明」:
fn longest(x: &str, y: &str) -> &str {
// │ │ └── 返回的引用:从 x、y 里来,能活多久?
// │ └── y 的有效范围
// └── x 的有效范围
//
// 编译器必须保证:返回引用的有效范围 ⊆ x 和 y 的有效范围的交集
// 'a 就是这个交集的名字
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
类比 Java:JVM 有逃逸分析,Rust 把同样的验证搬到了类型系统里,并要求写不出来的地方明说。
跟着做
步骤 1:复现一个真实的 E0106
cargo new life-demo
cd life-demo
// fn longest(x: &str, y: &str) -> &str {
// if x.len() > y.len() { x } else { y }
// }
// ← 取消注释:error[E0106] missing lifetime specifier
// 编译器不知道返回的引用活多久,拒绝放行
fn main() {}
报错原因:返回的引用可能来自 x 也可能来自 y,编译器无法确认它比 x、y 都活得短。
步骤 2:加上标注,让规则显式化
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let log1 = String::from("runtime panic");
let result;
{
let log2 = String::from("borrow check");
result = longest(&log1, &log2);
println!("较长者:{result}"); // OK:result 的使用在两者都存活的区间内
}
println!("{result}"); // ← 取消注释:error[E0597] log2 活得不够久
}
'a 是一个范围变量:编译器会把 'a 实例化为「实际调用中两个引用寿命的交集」。把最后的 println 解开,编译器用 E0597 告诉你交集覆盖不了这个使用点——标注没有改变任何对象的真实寿命,它只是让检查可执行。
步骤 3:三条省略规则——为什么平时不用写
fn first_word(s: &str) -> &str { // 实际签名:fn first_word<'a>(s: &'a str) -> &'a str
s.split_whitespace().next().unwrap_or("")
}
fn main() {
let line = String::from("hello rust world");
let w = first_word(&line);
println!("{w}");
}
省略规则:① 每个引用参数获得一个独立的生命周期参数;② 只有一个输入引用时,它被赋给所有输出引用;③ 方法有 &self 时,self 的生命周期赋给所有输出。规则覆盖不了的(如两个引用参数加返回引用),才要求显式标注。
步骤 4:结构体持有引用
struct Excerpt<'a> {
part: &'a str, // Excerpt 实例不能比它借用的文本活得久
}
impl<'a> Excerpt<'a> {
fn len(&self) -> usize { self.part.len() }
fn shout(&self) -> String { format!("{}!", self.part.to_uppercase()) }
}
fn main() {
let novel = String::from("call me ishmael. some years ago");
let first = novel.split('.').next().unwrap();
let e = Excerpt { part: first };
println!("{} ({} chars)", e.shout(), e.len());
}
结构体带 'a 参数的含义:这个实例的存在前提是「被引用的数据还活着」。把 novel 在 e 之前 drop 的代码会被编译器拒绝。
步骤 5:认识 'static
static MAX_USERS: u32 = 10_000; // 全局常量,程序全程有效
fn banner() -> &'static str {
"===== Mini Judge =====" // 字面量编译进二进制,永远有效
}
fn main() {
let s: &'static str = banner();
println!("{s} max={MAX_USERS}");
}
'static 表示整个程序运行期有效。字符串字面量都是 'static;遇到「需要 'static 但拿到短命数据」的报错,通常正确解法是改用所有权(String)而不是硬转。
讲清楚要点
- 标注不改变寿命,只描述寿命:
'a是编译期验证用的代数符号,运行时完全消失,零开销。 - 省略规则覆盖 90% 场景:一个引用进、一个引用出的函数全部免标注;两个引用参与、返回引用的函数必须写。
- 结构体的
'a是存在前提:Excerpt<'a>编译进类型系统后,「过期引用」在类型层面不可表达。 - 生命周期是泛型的一种:
fn longest<'a>和fn largest<T>同构,一个约束范围,一个约束类型。 - 遇到 'static 约束先想所有权:线程闭包要求
'static(第 9 课)、tokio spawn 同理,惯用解法是move把数据所有权搬进去,而不是延长引用。
你可能会懵的地方
-
生命周期标注会不会让对象活得更久? 完全不会。它不生成任何代码,纯粹是给借用检查器的证明材料;对象寿命仍由所有权决定。
-
为什么两个引用参数返回引用就必须写标注? 因为编译器推不出返回引用来自谁。
'a的作用是把「返回值与两个参数同寿命交集」这个业务事实告诉编译器——你比它懂业务,所以由你声明,由它验证。 -
'a 和 'static 是什么关系?
'a是范围变量,调用时被实例化为具体范围;'static是那个最大范围(整个程序期)。&'static str能赋给&'a str位置的变量,反之不行。 -
引用计数的 Arc 也要生命周期吗? Arc 拥有数据所有权,不涉及借用,不需要标注。生命周期只管「借用来的」。需要共享又怕生命周期麻烦时,Arc 是合法逃生通道,但先想想是不是在逃避正确建模。
-
Java 有对应概念吗? 没有直接对应。Java 的逃逸分析、对象晋升与 GC 代际是运行时机制;Rust 把等价的「引用是否逃逸、是否悬垂」验证编译期完成。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 给所有函数手写生命周期 | 无意义噪音 | 先不写,编译器要求时再补 |
| 返回局部变量的引用 | 必然悬垂,E0515 | 返回所有权类型,或返回 'static 字面量 |
结构体存引用却没写 'a | error[E0106] | struct S<'a> { part: &'a str } |
| 用 'static 硬转短命数据 | 数据其实已经死亡 | 改用 owned 类型(String/Box)传递所有权 |
混淆 'a 的位置:&'a T 写成 &T<'a> | 语法错误 | 标注属于引用本身:&'a str |
卡点急救
- error[E0106] missing lifetime specifier:编译器会列出它考虑过的省略规则;按提示补
'a,把返回引用与相关输入引用绑到同一'a。 - error[E0597] borrowed value does not live long enough:某内部作用域的值被外部引用。解法按优先级:把数据提到外层作用域、clone 一份、改为传所有权。
- 「expected lifetime 'a, found 'static」来回打架:通常是结构体/函数签名里把范围定死成 'a,调用处传了更长或更短的数据;先统一签名里所有引用用同一个 'a,再让具体调用去实例化。
面试常问
-
生命周期标注解决什么问题? 在无 GC 的前提下,让编译器机械地验证「所有引用都不会比被引用者活得久」,把悬垂指针这类 C/C++ 的运行时灾难变成编译错误,且运行时零开销。
-
为什么大部分函数不需要写生命周期? 省略规则三条(参数各占一个、单入参赋给输出、self 优先)能确定性地推导绝大多数签名;需要显式标注的场景本质是「编译器无法从签名推断返回引用的来源」。
-
'static 在什么场景出现? 字符串字面量与全局常量;
Box::leak之类的泄漏;以及线程/tokio spawn 的闭包约束——此时惯用 move 语义把所有权搬进闭包来满足。
自测三问
'a在fn longest<'a>(x: &'a str, y: &'a str) -> &'a str里被实例化成什么?- 三条省略规则分别是什么?举一个规则覆盖不了必须手写的签名。
struct Excerpt<'a> { part: &'a str }中'a对 Excerpt 实例的寿命意味着什么?
小结一句话
生命周期标注是写给借用检查器的范围代数:你声明业务事实,它机械验证,运行时零成本——这是 Rust 无 GC 又无悬垂引用的最后一环。
延伸
- The Rust Book ch10.3:生命周期语法
- 下一课:结构体与枚举——用类型建模评测结果,match 模式匹配登场。