生命周期
lifetime annotations、elision、常见模式。
生命周期
这一课你将能做到
- 解释生命周期标注的本体:它是对引用「有效范围」的描述,不是延长寿命的开关
- 掌握三条省略规则,判断什么时候必须显式写
'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());
}