Skip to content

生命周期

生命周期(Lifetime)就是引用的有效作用域。大多数时候无需手动声明,编译器会自动推导;但当存在多个引用、编译器无法推断其约束关系时,就需要手动标注。这和类型推导的逻辑完全类似:

  • 编译器大多数时候可以自动推导类型 → 同样,编译器大多数时候也可以自动推导生命周期
  • 当多种类型同时出现时,编译器要求手动标注类型 → 当多个生命周期存在且编译器无法推导时,就需要手动标注生命周期

生命周期的核心目的只有一个: 防止悬垂引用(Dangling Reference)——即防止程序使用一个已经被释放的内存地址。

悬垂引用与借用检查

看一个经典的悬垂引用例子:

rust
fn main() {
    let r;
    {
        let x = 5;
        r = &x;         // r 引用了 x
    }                   // x 在这里被释放
    println!("{}", r);  // ❌ r 现在引用了无效内存
}

编译器会拒绝这段代码,报告 x 活得不够久。原因是 Rust 内部有一个借用检查器(Borrow Checker),通过比较引用与被引用值的生命周期来验证所有引用的合法性:

rust
fn main() {
    let r;                // ------+-- 'a   r 的生命周期
                          //       |
    {                     //       |
        let x = 5;        // -+--'b|   x 的生命周期
        r = &x;           //  |    |
    }                     // -+    |   'b 在这里结束,x 被释放
                          //       |
    println!("{}", r);    //       |   r 还在('a),但引用的 'b 已结束
}                         // ------+

借用检查器发现: r 的生命周期 'a 比它引用的 x 的生命周期 'b 更长('b'a),引用一个比自己早结束的值,就是悬垂引用。

要修复这个问题,只需让被引用的值活得比引用更久:

rust
fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |   'a ⊆ 'b,安全!
    println!("{}", r);    //   |       |
                          // --+       |
}                         // ----------+

生命周期标注语法

生命周期标注并不改变任何引用的实际作用域,它只是给编译器提供信息,描述多个引用之间的约束关系。标注只是让编译器知道"这几个引用应该活得一样久",而不会真正延长或缩短任何值的寿命。

标注语法: 以 ' 开头,名称通常是单个小写字母,'a 最为常用。对于引用类型,生命周期标注位于 & 之后:

rust
&i32        // 普通引用
&'a i32     // 带显式生命周期的引用
&'a mut i32 // 带显式生命周期的可变引用

单独一个生命周期标注没有意义,它的价值在于描述多个引用之间的关系,例如:

rust
fn useless<'a>(first: &'a i32, second: &'a i32) {}
// 表示: first 和 second 至少都活得和 'a 一样久

函数中的生命周期标注

考虑一个返回两个字符串切片中较长者的函数:

rust
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

这段代码无法编译,错误提示:

error[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`

编译器在函数签名处无法判断返回值借用自 x 还是 y,也就无法验证调用方对返回值的使用是否安全。解决方法是手动标注生命周期:

rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

这里的 'a 告诉编译器: xy 和返回值至少都活得和 'a 一样久

注意,'a 并不代表"生命周期等于 'a",而是"大于等于 'a"。调用时,'a 的实际大小等于 xy 生命周期中较小的那个,返回值的生命周期也随之等于两者中较短的:

rust
fn main() {
    let string1 = String::from("long string is long");
    {
        let string2 = String::from("xyz");
        // 'a 等于 string2 的生命周期(两者中较短的)
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {}", result); // ✅ result 在 string2 的作用域内使用
    }
}

若将 result 移到 string2 作用域外使用,则编译失败:

rust
fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("{}", result); // ❌ string2 已释放,'a 要求 result 不能活得比 string2 久
}

只标注相关参数

生命周期标注只需覆盖与返回值有关的参数。如果函数永远只返回第一个参数,y 与返回值没有关系,就不需要为 y 标注:

rust
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

返回值的生命周期来源

函数返回引用时,生命周期只有两个来源:

  1. 来自函数参数 — 安全,通过生命周期标注告知编译器
  2. 来自函数体内新创建的值 — 这是悬垂引用,函数结束时该值被释放
rust
// ❌ 返回了函数体内临时创建的值的引用
fn bad<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str() // result 在函数结束时释放,引用随即失效
}

遇到这种情况,正确做法是返回所有权而非引用,把值转移给调用方:

rust
fn good(_x: &str, _y: &str) -> String {
    String::from("really long string") // 把所有权转移给调用方
}

结构体中的生命周期

结构体字段如果包含引用类型,必须为每个引用字段标注生命周期。语义是: 结构体实例的生命周期不能超过其引用字段所指向的数据:

rust
struct ImportantExcerpt<'a> {
    part: &'a str, // 'a 确保 ImportantExcerpt 不会比 part 引用的数据活得更久
}

fn main() {
    let novel = String::from("I am George...");
    let first_sentence = novel.split('.').next().expect("Could not find '.'");
    let i = ImportantExcerpt { part: first_sentence }; // ✅ novel 比 i 活得久
}

反过来,结构体比其引用的数据活得更久则编译失败:

rust
fn main() {
    let i;
    {
        let novel = String::from("I am George...");
        let first_sentence = novel.split('.').next().unwrap();
        i = ImportantExcerpt { part: first_sentence };
    } // novel 在这里释放
    println!("{:?}", i); // ❌ i 还在用,但 part 引用的数据已释放
}

生命周期省略规则

Rust 编译器内置了三条生命周期省略规则(Lifetime Elision Rules),规则能推导出所有引用的生命周期时无需手动标注,规则不适用时则报错要求手动标注。

规则适用范围内容
规则一输入生命周期每个引用参数各自获得一个独立的生命周期参数
规则二输出生命周期若只有一个输入生命周期,则赋给所有输出生命周期
规则三输出生命周期方法中有 &self&mut self,则 self 的生命周期赋给所有输出

记忆口诀: ① 每参一个 ② 独苗传全局 ③ Self 大过天

规则应用示例

示例一: 单参数函数,规则一 + 规则二足以推导完整,无需手动标注

rust
手写    fn first_word(s: &str) -> &str
规则一  fn first_word<'a>(s: &'a str) -> &str
规则二  fn first_word<'a>(s: &'a str) -> &'a str  ✅ 推导完成

示例二: 两参数函数,三条规则均无法推导返回值,必须手动标注

rust
手写   fn longest(x: &str, y: &str) -> &str
规则一 fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str
规则二 不适用(有两个输入生命周期)
规则三 不适用(不是方法,无 &self)
→ 编译器无法确定返回值的生命周期,报错

方法中的生命周期

为含有生命周期的结构体实现方法时,需要在 impl 后声明生命周期参数,语法与泛型一致:

rust
impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 { 3 }  // 返回值不是引用,无需标注

    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
        // 规则三: 返回值生命周期自动等于 &self 的生命周期,编译通过
    }
}

若需要返回值的生命周期与 announcement 参数一致(而不是 self),编译器无法自动推导,需要手动标注并添加生命周期约束:

rust
// 'a: 'b 表示 'a 至少活得和 'b 一样久
impl<'a: 'b, 'b> ImportantExcerpt<'a> {
    fn announce_and_return_part(&'a self, announcement: &'b str) -> &'b str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

也可以用 where 子句将约束分离出来,可读性更好:

rust
impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'b str
    where
        'a: 'b,
    {
        println!("Attention please: {}", announcement);
        self.part
    }
}

'a: 'b 是生命周期约束语法,与泛型约束 T: Trait 完全类似,读作"'a 至少活得和 'b 一样久"。约束的含义是: &'a self 是被引用方,所以引用它的 &'b str 必须活得更短,即 'b'a,写作 'a: 'b

静态生命周期('static)

'static 是 Rust 中最特殊的生命周期,拥有它的引用能与整个程序共存亡。所有字符串字面量都具有 'static 生命周期,因为它们被编译进二进制文件:

rust
let s: &'static str = "我活得和程序一样久";

遇到生命周期编译错误时,不要以为加上 'static 就万事大吉。应先思考: 是否存在悬垂引用?是否存在生命周期不匹配? 只有确认引用确实需要活得那么久时,才使用 'static。滥用 'static 可能掩盖真正的内存安全问题。

综合示例: 泛型 + Trait Bound + 生命周期

三者可以组合使用,在同一个函数签名中共存:

rust
use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() { x } else { y }
}

额外补充

枚举中的生命周期

rust
// 语义: Status 的生命周期不能超过它包含的字符串切片的生命周期
enum Status<'a> {
    Success,
    Error(&'a str), // 错误信息是借用的,不拥有所有权
}

Trait 实现中的生命周期

rust
// 语义: ImportantExcerpt 的生命周期不能超过它包含的字符串切片的生命周期
struct ImportantExcerpt<'a> {
    part: &'a str,
}

// 语义: ImportantExcerpt 实现了 Display Trait,且实现中使用了生命周期 'a
impl<'a> Display for ImportantExcerpt<'a> {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        write!(f, "{}", self.part)
    }
}

方法返回值与结构体生命周期解耦

结构体方法默认通过规则三将返回值生命周期绑定到 &self。但如果你明确知道返回的引用来自结构体字段(而非外部参数),可以直接标注字段的生命周期,让返回值的寿命超过方法调用的实例寿命:

rust
struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    // 返回 'a 而不是 self 的生命周期
    fn get_part(&self) -> &'a str {
        self.part
    }
}

fn main() {
    let data = String::from("很长寿的数据");
    let part_ref;
    {
        let excerpt = ImportantExcerpt { part: &data };
        part_ref = excerpt.get_part(); // 拿到标注为 'a(data 的寿命)的引用
    } // excerpt 在这里被销毁,但 part_ref 引用的是 data,仍然有效

    println!("{}", part_ref); // ✅ 安全!引用的数据(data)还活着
}

生命周期子类型化(Subtyping)

'a: 'b 不只是约束语法,也是 Rust 生命周期子类型化的体现: 如果 'a 的作用域完全覆盖 'b,那么在需要 'b 的地方,可以安全地使用 'a(长的可以当短的用,反之不行)。

符号: 'a: 'b
读作: 'a 至少活得和 'b 一样久('a 是 'b 的子类型)
逻辑: 若 'a 的作用域完全覆盖 'b,则 'a 可以安全地当成 'b 来使用

基于 MIT 协议发布