生命周期
一句话理解
生命周期是编译期的标注,用来告诉借用检查器「这个引用至少活多久」。
关键澄清:生命周期标注不会延长任何东西的寿命,它只是把「引用之间谁不能比谁先死」这层关系写出来,让编译器去核对。真正决定存活时长的仍然是作用域和所有权。
1. 为什么需要生命周期
编译器要保证引用永远不悬垂。看这段代码:
fn main() {
let r;
{
let x = 5;
r = &x; // ❌ E0597: x does not live long enough
}
println!("{}", r);
}x 在内部作用域结束时被 drop,r 就会悬垂。编译器通过比较”引用的使用点”和”被引用值的存活范围”拦住了它。
大多数时候这层分析是自动的;只有当一个函数有多个引用输入、又要返回引用时,编译器无法推断该返回哪一个的寿命,才需要你手写标注。
2. 语法与含义
&'a T // 一个在 'a 期间有效的不可变引用
&'a mut T // 一个在 'a 期间有效的可变引用'a 读作 “lifetime a”,是一个泛型参数(生命周期参数)。惯例名:'a、'b、'c……'static 是唯一的内置特殊值。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}这句话的准确含义是:返回值的有效期 ≤ 两个入参中较短的那个。它没有让 x 或 y 活得更久。
调用时,编译器会取两个实际生命周期中较短者绑定到 'a:
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("xyz");
result = longest(s1.as_str(), s2.as_str()); // 'a 被绑定为 s2 的寿命
println!("{}", result); // ✅ 在这里用没问题
}
// println!("{}", result); // ❌ 这里 s2 已经死了
}3. 省略规则(Elision)
绝大多数签名不用手写生命周期,因为编译器有三条规则:
- 每个省略的输入生命周期各得一个独立参数:
fn f(x: &str, y: &str)→fn f<'a, 'b>(x: &'a str, y: &'b str) - 如果恰好只有一个输入生命周期,它被赋给所有省略的输出生命周期
- 如果入参中有
&self或&mut self,self的生命周期被赋给所有省略的输出生命周期
所以下面这些都能编译:
fn first_word(s: &str) -> &str { ... } // 规则 2
impl Foo {
fn name(&self) -> &str { ... } // 规则 3
}而当规则无法决定时才报错:
fn longest(x: &str, y: &str) -> &str { ... }
// ❌ 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`什么时候该警觉
只要看到
missing lifetime specifier,先想清楚一件事:返回的引用到底来自哪个入参? 如果答案不是”来自某一个入参”,那么你大概率不该返回引用——改成返回拥有型值(String、Vec<T>)更简单。
4. 结构体持有引用
结构体里存引用必须标注,因为编译器无法自动推断字段活多久:
struct Excerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first = novel.split('.').next().unwrap();
let e = Excerpt { part: first };
println!("{}", e.part);
}含义:Excerpt 的实例不能比它引用的 novel 活得更久。这个约束在编译期成立,运行时不占任何空间——'a 会被擦除,没有运行时开销。
结构体存引用会传染
一旦某个结构体带生命周期参数,所有持有它的结构体、以及所有实现它的 trait 都可能要跟着加。业务代码里更常见的做法是直接持有
String,把生命周期参数留在工具函数和解析器这一层。
5. 'static 的两种含义
这是最容易混淆的一个点:
// 含义一:这个引用活得和程序一样久
let s: &'static str = "字符串字面量在二进制里,永远有效";
// 含义二:这个类型不包含任何非 'static 引用(作为 trait bound)
fn spawn_thing<T: 'static>(v: T) { ... }| 写法 | 含义 |
|---|---|
&'static T | 引用本身指向的数据活到程序结束(字面量、Box::leak) |
T: 'static | T 里不含有生命周期短于 'static 的引用;T 本身可以是拥有型值 |
fn takes_static<T: 'static>(t: T) {}
let owned = String::from("hi");
takes_static(owned); // ✅ String: 'static(它不借任何人)
let s = String::from("hi");
// takes_static(&s); // ❌ &String 不是 'static为什么
thread::spawn要求'static线程可能比创建它的作用域活得更久,所以闭包捕获的东西必须不借任何局部变量。这就是
thread::spawn的F: Send + 'static要求——它不是”要求活到世界末日”,而是”不许借用局部变量”。要么move走所有权,要么用std::thread::scope(Rust 1.63+)显式限定线程不越过作用域。
6. 常见错误对照
| 错误码 | 报错文本 | 原因 | 修法 |
|---|---|---|---|
E0106 | missing lifetime specifier | 多个输入引用 + 返回引用,无法推断 | 手写 <'a> 并指出返回来自哪个入参 |
E0597 | does not live long enough | 被引用值先于引用结束 | 提升被引用值的作用域,或改成拥有型 |
E0515 | cannot return reference to local variable | 返回局部变量的引用 | 返回 String/Vec<T>,或把引用作为参数传进来 |
E0716 | temporary value dropped while borrowed | 借用了临时值 | 先用 let 绑定临时值再借用 |
E0621 | explicit lifetime required in the type of … | 结构体字段缺少标注 | 给结构体加生命周期参数 |
E0716 的典型形态:
// ❌ 临时 String 在本行结束就死了
let s = String::from("hi").as_str();
// ✅ 先绑定住所有者
let tmp = String::from("hi");
let s = tmp.as_str();7. 设计上的建议
- 优先避免生命周期参数:返回拥有型值,或让调用方传入可变引用做原地写入
- 需要返回引用时,先确认它确实来自某个入参
&'static str是便宜好用的:常量表、错误消息、枚举名字都可以用它- 不要让生命周期标注污染整个业务层:它主要属于解析器、迭代器、零拷贝视图这类组件
8. 小结
关键判断
- 生命周期标注只描述约束,不改变寿命
- 三条省略规则覆盖了绝大多数签名,手写标注通常意味着”返回引用来源于哪个入参”说不清
&'static T和T: 'static是两件事,后者只是”不含短命引用”- 业务代码里,能返回拥有型就别返回引用