生命周期
生命周期标注描述的是引用之间的关系,以及常见编译失败场景的破法。
生命周期深入
'a 是「区间约束」,不是「具体时长」
生命周期(lifetime)的名字有误导性:'a 不代表某个具体纳秒数,它代表 「某段代码区域里引用保证有效」这个约束。编译器做的是区域包含关系(region inclusion)求解, 而不是计算时长。
🧠 原理:标在函数签名上的生命周期是给调用者的契约,同时是给函数体的许可。
- 对调用者:
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str承诺「返回值不会比a、b中较短的活得久」。- 对函数体:函数体可以把
a、b、返回值都当作同一个区域'a来用。因此标注不会让引用活得更久,它只会让编译器接受或拒绝你的代码。这是本章最重要的结论: 生命周期是描述,不是延长。
rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() >= y.len() { x } else { y }
}
fn main() {
let owned = String::from("hello world");
let borrowed = "hi"; // &'static str 也能当作更短的生命周期使用
println!("{}", longest(&owned, borrowed));
}结构体持有引用
结构体如果存引用,必须给结构体本身加生命周期参数:
rust
struct Parser<'a> {
input: &'a str,
position: usize,
}
impl<'a> Parser<'a> {
fn new(input: &'a str) -> Self {
Parser { input, position: 0 }
}
// 返回 'a 而不是 &self:token 直接指向原输入,与 Parser 的存活无关
fn rest(&self) -> &'a str {
&self.input[self.position..]
}
// 省略规则让它绑定到 &self,返回值随 Parser 的借用而失效
fn input_len(&self) -> usize {
self.input.len()
}
}
fn main() {
let text = String::from("alpha beta");
let parser = Parser::new(&text);
println!("{} {}", parser.rest(), parser.input_len());
}⚠️ 陷阱:
Parser<'a>的生命周期是「这个 Parser 实例不能比它借用的字符串活得久」这一约束的载体。 一旦text被drop,编译器会拒绝让parser继续存在——它的所有成员都是悬垂指针的候选。
生命周期省略(elision)三条规则
写 &str 而不写 &'a str 时不等于「没有生命周期」,而是编译器按三条规则补全:
- 每个被省略的输入生命周期各获得一个独立的生命周期参数:
fn f(x: &str, y: &str)→fn f<'a, 'b>(x: &'a str, y: &'b str)。 - 如果恰好只有一个输入生命周期(无论省略与否),它被赋给所有被省略的输出生命周期:
fn f(x: &str) -> &str→fn f<'a>(x: &'a str) -> &'a str。 - 如果有多个输入生命周期,但其中一个是
&self或&mut self,则self的生命周期被赋给所有 被省略的输出生命周期:fn f(&self, x: &str) -> &str→fn f<'a, 'b>(&'a self, x: &'b str) -> &'a str。
规则用尽仍无法确定输出生命周期时,就必须手写标注(E0106)。
rust
struct Book {
title: String,
}
impl Book {
// 规则 3:返回的 &str 绑定 &self,与参数无关;所以只能返回 self 的数据
fn first_char(&self) -> &str {
&self.title[..1]
}
// 想返回「两个来源里较长者」,就必须显式把两者标成同一个生命周期
fn pick_longer_title<'a>(&'a self, other: &'a str) -> &'a str {
if self.title.len() >= other.len() { &self.title } else { other }
}
}
fn main() {
let book = Book { title: String::from("rust") };
println!("{}", book.first_char());
println!("{}", book.pick_longer_title("golang"));
}输出:
r golang
⚠️ 陷阱:省略规则 3 有个容易踩的后果——返回值的生命周期被钉死在
&self上, 于是「返回other」的写法直接报错,而且报错信息完全不提省略规则:
rust
// ❌ 无法编译:省略规则 3 把返回值钉在 &self 上,返回 other 就不满足
struct Book {
title: String,
}
impl Book {
// 编译器的推断结果等价于 fn pick_longer_title<'a, 'b>(&'a self, other: &'b str) -> &'a str
fn pick_longer_title(&self, other: &str) -> &str {
if self.title.len() >= other.len() { &self.title } else { other }
}
}
fn main() {
let book = Book { title: String::from("rust") };
println!("{}", book.pick_longer_title("golang"));
}error: lifetime may not live long enough
--> src/main.rs:4:67
|
3 | fn pick_longer_title(&self, other: &str) -> &str {
| - - let's call the lifetime of this reference `'1`
| |
| let's call the lifetime of this reference `'2`
4 | if self.title.len() >= other.len() { &self.title } else { other }
| ^^^^^ method was
| supposed to return
| data with lifetime `'2`
| but it is returning
| data with lifetime `'1`
help: consider introducing a named lifetime parameter and update trait if needed
|
3 | fn pick_longer_title<'a>(&self, other: &'a str) -> &'a str {
| ++++ ++ ++⚠️ 陷阱:规则 3 只在存在
&self时生效。fn pick(a: &str, b: &str) -> &str会直接报 E0106, 因为编译器拒绝替你猜「返回值来自a还是b」。这正是「E0106 missing lifetime specifier」的例子。
'static 的两种含义
'static 有两个不同的用法,混用是最常见的误解来源:
- 作为引用的生命周期:
&'static str表示「这段数据在整个程序运行期都有效」。 字符串字面量会被编进二进制,因此天然是&'static str。 - 作为 trait bound:
T: 'static表示「T里不包含任何非'static的借用」。 它不是要求T活得和程序一样久——String、Vec<u8>、i32都满足T: 'static, 因为它们拥有自己的数据、不含借用。
rust
fn store<T: std::fmt::Debug + 'static>(value: T) -> Box<dyn std::fmt::Debug + 'static> {
Box::new(value)
}
fn main() {
// 字面量是 &'static str
let literal: &'static str = "in binary";
println!("{}", literal);
// String 满足 T: 'static,因为它不借用任何东西
let owned = String::from("owned");
println!("{:?}", store(owned));
// 借用局部变量不满足 T: 'static
let local = String::from("local");
let reference: &str = &local;
println!("{}", reference);
// 下面这行会报 E0597:`local` does not live long enough
// println!("{:?}", store(reference));
}Box<dyn Error + 'static>:Box<dyn Error> 里的 + 'static 是默认的(trait object 省略 生命周期时默认 'static)。这意味着放进 Box<dyn Error> 的错误类型不能借用局部数据:
rust
use std::error::Error;
// 必须显式写 T: 'static,否则 E0310(见「`E0310 lifetime bound not satisfied`」)
fn wrap<T: Error + 'static>(err: T) -> Box<dyn Error + 'static> {
Box::new(err)
}
fn main() {
let boxed = wrap(std::fmt::Error);
println!("{boxed}");
let erased: Box<dyn Error> = boxed;
println!("{erased}");
}🧠 原理:
T: 'static的准确定义是「T的所有生命周期参数都 outlive'static」。 对拥有所有权的类型,这个条件自动成立。所以Box<dyn Error + 'static>能装std::io::Error、String(借用的错误则需要写Box<dyn Error + 'a>)。如果在异步/并发上下文看到
'static满天飞(例如tokio::spawn),原因就是 任务可能比创建它的作用域活得久,所以它借用的一切都必须能活到进程结束或拥有所有权。
生命周期子类型与变型(variance)
'long: 'short 读作「'long 比 'short 长」,此时 &'long T 可以当作 &'short T 使用 (引用是协变的)。变型(variance) 描述的就是「类型构造器如何随参数的子类型关系变化」:
- 协变(covariant):参数变小,整体变小(
A <: B ⇒ F<A> <: F<B>)。 - 逆变(contravariant):参数变小,整体变大。
- 不变(invariant):必须完全相同。
| 类型 | 变型 | 直觉 |
|---|---|---|
&'a T | 对 'a 协变,对 T 协变 | 只能读,别人不会通过你往里写 |
&'a mut T | 对 'a 协变,对 T 不变 | 能写:写入会污染原本更长的类型 |
Box<T>、Vec<T> | 对 T 协变 | 拥有所有权,只按值读 |
Cell<T>、RefCell<T>、UnsafeCell<T> | 对 T 不变 | 内部可变性:能在背后把 T 换掉 |
fn(T) -> U | 对 T 逆变,对 U 协变 | 参数由调用者提供,方向相反 |
*const T / *mut T | 协变 / 不变 | 裸指针按可变性区分 |
dyn Trait + 'a | 对 'a 协变 | trait object 上带的生命周期是上界 |
PhantomData<T> | 跟随 T | 用于手工声明变型 |
为什么 &mut T 必须对 T 不变——下面这段代码如果被接受,就会产生悬垂引用:
rust
// ❌ 无法编译:如果 &mut &'a str 对 'a 协变,r 会被写成指向已释放的 s
fn evil<'a>(slot: &mut &'a str, value: &'a str) {
*slot = value;
}
fn main() {
let mut r: &'static str = "static";
let s = String::from("temporary");
evil(&mut r, &s);
println!("{r}"); // 若编译通过,此处读的是已经 drop 的 s
}error[E0597]: `s` does not live long enough
--> src/main.rs:9:18
|
4 | let mut r: &'static str = "static";
| ------------ type annotation requires that `s` is borrowed for `'static`
5 | let s = String::from("temporary");
| - binding `s` declared here
9 | evil(&mut r, &s);
| ^^ borrowed value does not live long enough推理链:假设 &mut &'a str 对 'a 协变,则 &mut &'static str 可以「降级」成 &mut &'short str 传进 evil;evil 随后把 &'short str 写回去,r 就变成指向短命数据的长生命周期引用, 而类型系统仍认为它是 &'static str——读时即悬垂。所以 &mut T 对 T 必须不变。
Cell<T> / RefCell<T> 同理:set 能在 &self 下替换内容,如果对 T 协变,就能把短引用塞进长容器:
rust
// ❌ 无法编译:Cell<&'static str> 不能当作 Cell<&'a str> 用
use std::cell::Cell;
fn set<'a>(cell: &Cell<&'a str>, value: &'a str) {
cell.set(value);
}
fn main() {
let cell: Cell<&'static str> = Cell::new("static");
let s = String::from("temporary");
set(&cell, &s); // 若 Cell 协变,cell 里就存了悬垂引用
println!("{}", cell.get());
}error[E0597]: `s` does not live long enough
|
4 | let cell: Cell<&'static str> = Cell::new("static");
| ------------------ type annotation requires that `s` is borrowed for `'static`
6 | set(&cell, &s);
| ^^ borrowed value does not live long enough逆变方向的一个可编译例子(fn(&'a str) 接受短引用,因此能顶替要求长引用的位置):
rust
struct Holder<'a> {
f: fn(&'a str) -> usize,
}
fn any_len(s: &str) -> usize {
s.len()
}
// Holder<'a> 对 'a 逆变:Holder<'a> 可以当作 Holder<'static> 使用
fn as_static<'a>(holder: Holder<'a>) -> Holder<'static> {
holder
}
fn main() {
let holder = Holder { f: any_len };
let fixed: Holder<'static> = as_static(holder);
println!("{}", (fixed.f)("abc"));
}反过来 fn shrink<'a>(h: Holder<'static>) -> Holder<'a> { h } 会报 returning this value requires that 'a must outlive 'static,这正是逆变的体现。
⚠️ 陷阱:变型不是你每天要操心的东西,但它解释了为什么「
&mut参数换个生命周期就过不了」 这类报错。遇到lifetime may not live long enough而直觉觉得「明明更短了」,先想变型。
什么时候不该写生命周期
生命周期标注有成本:它把公理写成噪音,还会意外收紧 API。以下情况不要手写:
rust
// 噪音写法:编译器会自动补出 'a,写出来只是干扰阅读
fn bad<'a>(text: &'a str) -> &'a str {
text
}
// 推荐写法:省略后语义完全一样
fn good(text: &str) -> &str {
text
}
// 噪音写法:结构体没有存引用,就不需要生命周期参数
struct Config<'a> {
name: String,
_marker: std::marker::PhantomData<&'a ()>,
}
// 推荐写法:存什么标什么
struct CleanConfig {
name: String,
}
fn main() {
println!("{}", good("hi"));
let _c = CleanConfig { name: String::from("app") };
let _d: Config<'static> = Config { name: String::new(), _marker: std::marker::PhantomData };
}判断标准:只要返回值/字段里没有借用、或者借用关系能被三条省略规则唯一确定,就别写。 只有一个输入生命周期、或返回引用必然来自 &self 时,省略总是安全的。
🧠 原理:写标注有三个真实动机:
- 返回值同时借用多个输入,必须说明关系(
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str);- 结构体/枚举要存引用,必须声明(
struct Parser<'a>);- 想表达比省略规则更宽或更窄的约束(例如
fn f<'a>(x: &'a str, y: &str) -> &'a str)。除此之外的标注都应删掉。
常见「生命周期打不过编译器」的场景与破法
结构体自引用
rust
// ❌ 无法编译:结构体不能同时拥有数据和对该数据的引用
struct SelfRef<'a> {
text: String,
slice: &'a str,
}
fn make() -> SelfRef<'static> {
let text = String::from("hello");
SelfRef { slice: &text[..], text }
}
fn main() {
println!("{}", make().slice);
}error[E0515]: cannot return value referencing local variable `text`
--> src/main.rs:8:5
|
8 | SelfRef { slice: &text[..], text }
| ^^^^^^^^^^^^^^^^^^----^^^^^^^^^^^^
| | |
| | `text` is borrowed here
| returns a value referencing data owned by the current function
error[E0505]: cannot move out of `text` because it is borrowed
|
8 | SelfRef { slice: &text[..], text }
| ----------------------------^^^^--
| | | |
| | | move out of `text` because it is borrowed
| returning this value requires that `text` is borrowed for `'static`原因:自引用类型一旦被移动(Rust 默认语义),内部引用就指向旧地址,成为悬垂指针。 Rust 的借用检查器不允许这种「地址敏感」的结构在安全代码里存在。
破法(按推荐顺序):
- 存索引或 id 而不是引用(绝大多数场景的正确答案):
struct Doc { text: String, spans: Vec<(usize, usize)> },用时再切片。 - 分层:让所有者在外层,借用者作为临时值传递(
Parser<'a>模式)。 - 用
Rc/Arc共享所有权,把「自引用」变成「两个所有者」。 - 改用现成 crate:
cargo add ouroboros(或self_cell),它用宏 +unsafe生成安全的 自引用类型,代价是访问方式变成闭包风格。 Pin+unsafe:一旦数据被Pin住就再也不能移动,自引用才成立。 这是自引用 future(async状态机)的底层机制,手写需要unsafe,见〈异步编程〉。
返回引用局部变量(E0515)→ 返回拥有所有权的值
rust
// ❌ 无法编译:局部变量在函数返回时就 drop 了
fn bad() -> &'static str {
let owned = String::from("temporary");
&owned
}
fn main() {
println!("{}", bad());
}error[E0515]: cannot return reference to local variable `owned`
--> src/main.rs:3:5
|
3 | &owned
| ^^^^^^ returns a reference to data owned by the current function修法:把所有权交出去,而不是交出引用。
rust
fn good() -> String {
String::from("temporary")
}
// 或者让调用者提供存放位置,函数只写回引用指向的内容
fn fill(buffer: &mut String) {
buffer.clear();
buffer.push_str("temporary");
}
fn main() {
println!("{}", good());
let mut buffer = String::new();
fill(&mut buffer);
println!("{buffer}");
}💡 对照:Java/C#/Python 返回对象引用天然安全(有 GC)。Rust 没有 GC, 所以「返回引用」必须证明被引用数据活得够久;证明不了就返回所有权。 这是 Rust 代码里
-> String比-> &str常见的原因,也是性能取舍的自觉选择。
迭代器与借用冲突 → 先 collect()
rust
// ❌ 无法编译:迭代期间试图修改同一个 Vec
fn main() {
let mut numbers = vec![1, 2, 3, 4];
for n in &numbers {
if *n == 2 {
numbers.push(99);
}
}
println!("{numbers:?}");
}error[E0502]: cannot borrow `numbers` as mutable because it is also borrowed as immutable
--> src/main.rs:4:22
|
3 | for n in &numbers {
| -------- immutable borrow occurs here
4 | if *n == 2 { numbers.push(99); }
| ^^^^^^^^^^^^^^^^ mutable borrow occurs here修法一:先收集成拥有所有权的集合(collect()),再遍历修改原容器。
rust
fn main() {
let mut numbers = vec![1, 2, 3, 4];
// 先把要插入的值算出来,借用结束
let extra: Vec<i32> = numbers.iter().filter(|n| **n == 2).map(|_| 99).collect();
numbers.extend(extra);
println!("{numbers:?}");
}修法二:用索引遍历,或 retain/drain 这类「一次性占用」的 API。
rust
fn main() {
let mut numbers = vec![1, 2, 3, 2, 4];
// 借用只在 filter 调用内存在,循环结束即释放
numbers.retain(|n| *n != 2);
let mut index = 0;
while index < numbers.len() {
if numbers[index] == 3 {
numbers.push(30);
}
index += 1;
}
println!("{numbers:?}");
}输出:
[1, 4, 30]
🧠 原理:迭代器是惰性的,它的生命周期覆盖整个
for循环。想把「读一遍」和「改一次」分开, 就必须在某处结束借用:collect()把结果搬到新容器,借用随之结束。 这也是Vec::iter().map(...).collect::<Vec<_>>()这个惯用法无处不在的原因。
E0716 temporary value dropped while borrowed
rust
// ❌ 无法编译:临时 String 在语句结束时就 drop 了
fn main() {
let text = String::from("hi").as_str();
println!("{text}");
}error[E0716]: temporary value dropped while borrowed
--> src/main.rs:2:13
|
2 | let text = String::from("hi").as_str();
| ^^^^^^^^^^^^^^^^^^ - temporary value is freed at the end of this statement
| |
| creates a temporary value which is freed while still in use
3 | println!("{text}");
| - borrow later used here
help: consider using a `let` binding to create a longer lived value
|
2 ~ let binding = String::from("hi");
3 ~ let text = binding.as_str();修法:把临时值绑定到 let,延长它的作用域(编译器的 help 已经给了答案)。
rust
fn main() {
let binding = String::from("hi");
let text = binding.as_str();
println!("{text}");
}2024 edition 的重要行为变化:if let 的临时值作用域被收窄了(RFC 3606)。 在 2021 里,if let scrutinee 产生的临时值活到整个 if let 语句结束(包含 else 块); 从 2024 起,它们在进入 else 之前就被 drop。看下面这段代码:
rust
use std::cell::RefCell;
fn main() {
let cell: RefCell<Vec<i32>> = RefCell::new(Vec::new());
if let Some(first) = cell.borrow().first() {
println!("first = {first}");
} else {
// 2021:临时 Ref 仍活着 → RefCell 运行期 panic: already borrowed
// 2024:临时 Ref 已在进入 else 前 drop → 正常执行
cell.borrow_mut().push(4);
println!("else branch pushed, len = {}", cell.borrow().len());
}
println!("end of main");
}| edition | 结果 |
|---|---|
| 2021 | 编译通过,运行到 else 时 panic:RefCell already borrowed |
| 2024 | 正常输出 else branch pushed, len = 1 / end of main |
if let 与 match 的差别正在这里:match scrutinee 的临时值仍然活到整个 match 语句结束, 等价于 2021 的 if let 行为。所以:
rust
use std::cell::RefCell;
fn main() {
let cell: RefCell<Vec<i32>> = RefCell::new(Vec::new());
// match 保留了旧的「临时值活到语句结束」行为
match cell.borrow().first() {
Some(first) => println!("first = {first}"),
None => {
// 2021 的 if let 在这里 panic;改成 match 后 2024 也一样 panic
cell.borrow_mut().push(4);
}
}
println!("done");
}输出(2021 与 2024 表现一致):
thread 'main' panicked at src/main.rs:8:13: RefCell already borrowed
⚠️ 陷阱:
match版本能通过编译,但运行到None分支就 panic——这恰好复现了 2021 的if let行为。 如果你的代码依赖 2021 的旧行为(例如故意让读锁活到else之后), 迁移时用cargo fix --edition,它借助if_let_rescopelint 提示需要改成match。
同类问题还有一个 tail expression(尾表达式)变体:块的最后一个表达式里的临时值, 在 2021 里活在局部变量之后才 drop,在 2024 里提前到局部变量之前 drop:
rust
use std::cell::RefCell;
fn probe() {
let cell: RefCell<Vec<i32>> = RefCell::new(Vec::new());
// 2021:这里的 if let 是函数的尾表达式,临时 Ref 活到 cell 之后 → E0597
// 2024:临时 Ref 先于 cell 被 drop → 编译通过
if let Some(first) = cell.borrow().first() {
println!("first = {first}");
} else {
println!("empty");
}
}
fn main() {
probe();
}2021 edition 下的报错原文
error[E0597]: `cell` does not live long enough
--> src/main.rs:4:26
|
3 | let cell: RefCell<Vec<i32>> = RefCell::new(Vec::new());
| ---- binding `cell` declared here
4 | if let Some(first) = cell.borrow().first() {
| ^---------
| |
| borrowed value does not live long enough
| a temporary with access to the borrow is created here ...
...
| `cell` dropped here while still borrowed
| ... and the borrow might be used here, when that temporary is dropped
| and runs the destructor for type `Ref<'_, Vec<i32>>`
help: consider adding semicolon after the expression so its temporaries are dropped sooner2021 的修法:把 if let 挪进一个带分号的语句,或拆成 let ... = ...; 两步。
闭包捕获的生命周期与 move
闭包默认按引用捕获,因此闭包类型带上被捕获变量的生命周期。两条实际影响:
- 闭包借用变量期间,其他地方不能再借用该变量。
- 要把闭包返回出去,必须
move,否则闭包借的是局部变量,返回即悬垂。
rust
// ❌ 无法编译:闭包持有 words 的可变借用,直到最后一次使用
fn main() {
let mut words = vec![String::from("a")];
let mut pop_one = || words.pop();
println!("{:?}", pop_one());
words.push(String::from("b")); // E0499:第二次可变借用
println!("{:?}", pop_one());
}error[E0499]: cannot borrow `words` as mutable more than once at a time
--> src/main.rs:5:5
|
3 | let mut pop_one = || words.pop();
| -- ----- first borrow occurs due to use of `words` in closure
4 | println!("{:?}", pop_one());
5 | words.push(String::from("b"));
| ^^^^^ second mutable borrow occurs here
6 | println!("{:?}", pop_one());
| ------- first borrow later used here正确写法(每个例子都可编译):
rust
// move 把 n 的所有权移进闭包,闭包才能活着离开函数
fn counter() -> impl FnMut() -> u32 {
let mut n = 0;
move || {
n += 1;
n
}
}
// 闭包借用外部数据时,用生命周期参数把契约写出来
fn make_printer<'a>(text: &'a str) -> impl Fn() -> String + 'a {
move || text.to_uppercase()
}
fn main() {
let mut next = counter();
println!("{} {} {}", next(), next(), next());
let name = String::from("rust");
let printer = make_printer(&name);
println!("{}", printer());
// 只想读一个 Copy 值,move 与不 move 无差别,但 move 能让闭包活得更久
let limit = 3;
let check = move |n: u32| n < limit;
println!("{}", check(2));
}输出:
1 2 3 RUST true
🧠 原理:
move只影响「怎么捕获」,不影响「怎么调用」。闭包实现哪个Fn*trait 由其函数体决定: 只读 →Fn;改捕获状态 →FnMut;消费捕获值 →FnOnce。counter返回impl FnMut, 因此调用前变量必须let mut。返回借用的闭包时,必须把生命周期写进 RPIT(
+ 'a),因为编译器不会替你猜到闭包依赖text。