Skip to content

生命周期

生命周期标注描述的是引用之间的关系,以及常见编译失败场景的破法。

生命周期深入

'a 是「区间约束」,不是「具体时长」

生命周期(lifetime)的名字有误导性:'a 不代表某个具体纳秒数,它代表 「某段代码区域里引用保证有效」这个约束。编译器做的是区域包含关系(region inclusion)求解, 而不是计算时长。

🧠 原理:标在函数签名上的生命周期是给调用者的契约,同时是给函数体的许可

  • 对调用者:fn longest<'a>(a: &'a str, b: &'a str) -> &'a str 承诺「返回值不会比 ab 中较短的活得久」。
  • 对函数体:函数体可以把 ab、返回值都当作同一个区域 '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 实例不能比它借用的字符串活得久」这一约束的载体。 一旦 textdrop,编译器会拒绝让 parser 继续存在——它的所有成员都是悬垂指针的候选。

生命周期省略(elision)三条规则

&str 而不写 &'a str 时不等于「没有生命周期」,而是编译器按三条规则补全

  1. 每个被省略的输入生命周期各获得一个独立的生命周期参数fn f(x: &str, y: &str)fn f<'a, 'b>(x: &'a str, y: &'b str)
  2. 如果恰好只有一个输入生命周期(无论省略与否),它被赋给所有被省略的输出生命周期fn f(x: &str) -> &strfn f<'a>(x: &'a str) -> &'a str
  3. 如果有多个输入生命周期,但其中一个是 &self&mut self,则 self 的生命周期被赋给所有 被省略的输出生命周期fn f(&self, x: &str) -> &strfn 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 boundT: 'static 表示「T不包含任何非 'static 的借用」。 它不是要求 T 活得和程序一样久——StringVec<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::ErrorString(借用的错误则需要写 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) -> UT 逆变,对 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 传进 evilevil 随后把 &'short str 写回去,r 就变成指向短命数据的长生命周期引用, 而类型系统仍认为它是 &'static str——读时即悬垂。所以 &mut TT 必须不变。

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 时,省略总是安全的。

🧠 原理:写标注有三个真实动机:

  1. 返回值同时借用多个输入,必须说明关系(fn longest<'a>(a: &'a str, b: &'a str) -> &'a str);
  2. 结构体/枚举要引用,必须声明(struct Parser<'a>);
  3. 想表达比省略规则更宽或更窄的约束(例如 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 的借用检查器不允许这种「地址敏感」的结构在安全代码里存在。

破法(按推荐顺序):

  1. 存索引或 id 而不是引用(绝大多数场景的正确答案): struct Doc { text: String, spans: Vec<(usize, usize)> },用时再切片。
  2. 分层:让所有者在外层,借用者作为临时值传递(Parser<'a> 模式)。
  3. Rc/Arc 共享所有权,把「自引用」变成「两个所有者」。
  4. 改用现成 cratecargo add ouroboros(或 self_cell),它用宏 + unsafe 生成安全的 自引用类型,代价是访问方式变成闭包风格。
  5. 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 letmatch 的差别正在这里: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_rescope lint 提示需要改成 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 sooner

2021 的修法:把 if let 挪进一个带分号的语句,或拆成 let ... = ...; 两步。

闭包捕获的生命周期与 move

闭包默认按引用捕获,因此闭包类型带上被捕获变量的生命周期。两条实际影响:

  1. 闭包借用变量期间,其他地方不能再借用该变量
  2. 要把闭包返回出去,必须 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;消费捕获值 → FnOncecounter 返回 impl FnMut, 因此调用前变量必须 let mut

返回借用的闭包时,必须把生命周期写进 RPIT(+ 'a),因为编译器不会替你猜到闭包依赖 text


内容以 rustc 1.98.1 · Rust 2024 edition 为基准