讲解
生命周期(lifetime)是借用检查器的记账系统:每个引用都有一个「它指向的数据至少活到什么时候」的范围,编译器靠它证明所有引用在使用时必然有效。好消息是绝大多数情况下编译器能自己推出来(生命周期省略规则),你根本不用写;只有当「返回值引用到底借自哪个参数」这类关系有歧义时,才需要手动标注。
标注语法是撇号加小写名:'a。fn longest<'a>(x: &'a str, y: &'a str) -> &'a str 读作:「x、y 和返回值共享同一个生命周期下限 a——返回值的有效期不会超过两个参数中较短的那个」。注意生命周期标注不改变任何值的实际存活时间,它只是向编译器描述引用之间的关系,让编译器有依据批准或拒绝代码。
三条省略规则值得知道(输入引用各占一个生命周期;只有一个输入引用时它赋给所有输出;方法里 &self 的生命周期赋给所有输出)——正因为它们,前面章节写的 calculate_length(s: &String) -> usize 才不用标注。还有一个特殊的 'static:活到程序结束,所有字符串字面量都是 &'static str。常见误解是把 'static 当「万能续命」乱用——它表示「真的活到程序结束」,不是「让编译器闭嘴」。
示例
需要标注的经典场景:返回两个引用中较长的那个:
// 'a 声明:返回值的生命周期不超过 x 和 y 中较短者
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("长长的字符串");
let s2 = String::from("短");
let result = longest(&s1, &s2);
assert_eq!(result, "长长的字符串");
// 不同作用域也成立:返回值的有效期不超过较短的那个输入
let s3 = String::from("另一个长字符串");
let r;
{
let s4 = String::from("短");
r = longest(&s3, &s4);
println!("{r}");
assert_eq!(r, "另一个长字符串"); // s4 还活着,r 在这里有效
}
// 出了内层作用域 s4 已释放,r 随之失效——再用就是编译错误
}
试图返回局部变量的引用会被直接拒绝(构建脚本验证这段确实编译失败):
// compile-fail: 返回局部变量的引用(悬垂引用)
fn dangle<'a>() -> &'a String {
let s = String::from("局部字符串");
&s
}
fn main() {
let r = dangle();
println!("{r}");
}
报错大意(E0515,s 是局部变量,函数结束时就会被释放):
error[E0515]: cannot return reference to local variable `s`
--> main.rs:4:5
|
4 | &s
| ^^ returns a reference to data owned by the current function
常见坑
- 以为标注能延长生命:生命周期标注只是「描述关系」,数据该什么时候释放还是什么时候释放;想让数据活久点,改的是所有权不是标注。
- 动不动 'static:收到「expects 'static」报错时,正确解法通常是改设计(传所有权、用 String 代替 &str),而不是把类型硬掰成 'static。
- 每个函数都手写标注:先看能不能靠省略规则通过——能推断就不标注,和类型标注的原则一样。
- 绕开问题用 unsafe:生命周期报错意味着设计上真的有悬垂风险;正确回应是理清数据归谁所有,而不是关检查。
小结
生命周期是引用的有效期证明;多数场景编译器自动省略;手写标注描述「返回值借自谁」;'static 表示活到程序结束。至此 Rust 所有权三件套(所有权、借用、生命周期)全部就位。下一章轻松一下:闭包与迭代器。