生命周期

一句话理解

生命周期是编译期的标注,用来告诉借用检查器「这个引用至少活多久」。

关键澄清:生命周期标注不会延长任何东西的寿命,它只是把「引用之间谁不能比谁先死」这层关系写出来,让编译器去核对。真正决定存活时长的仍然是作用域和所有权。

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)

绝大多数签名不用手写生命周期,因为编译器有三条规则:

  1. 每个省略的输入生命周期各得一个独立参数:fn f(x: &str, y: &str) → fn f<'a, 'b>(x: &'a str, y: &'b str)
  2. 如果恰好只有一个输入生命周期,它被赋给所有省略的输出生命周期
  3. 如果入参中有 &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: 'staticT 里不含有生命周期短于 '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. 常见错误对照

错误码报错文本原因修法
E0106missing lifetime specifier多个输入引用 + 返回引用,无法推断手写 <'a> 并指出返回来自哪个入参
E0597does not live long enough被引用值先于引用结束提升被引用值的作用域,或改成拥有型
E0515cannot return reference to local variable返回局部变量的引用返回 String/Vec<T>,或把引用作为参数传进来
E0716temporary value dropped while borrowed借用了临时值先用 let 绑定临时值再借用
E0621explicit 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. 小结

关键判断

  1. 生命周期标注只描述约束,不改变寿命
  2. 三条省略规则覆盖了绝大多数签名,手写标注通常意味着”返回引用来源于哪个入参”说不清
  3. &'static T 和 T: 'static 是两件事,后者只是”不含短命引用”
  4. 业务代码里,能返回拥有型就别返回引用

相关笔记