首页 / Rust 入门教程 / 生命周期(Lifetimes)入门

Rust 入门教程

生命周期(Lifetimes)入门

本教程共 78 篇 · 第 46 篇 · 更新于 2026-08-08 · 约 12 分钟阅读

RustRust 入门教程生命周期Lifetime悬垂引用借用检查static标注

本节目标:理解生命周期是引用的有效作用域,弄清它如何防止悬垂引用,学会在函数和结构体里手动标注生命周期,并知道编译器何时能自动推断。

生命周期大概是 Rust 里最让新手头大的概念。但别慌——它解决的其实是一件很朴素的事:保证你手里的引用,指向的数据还活着。

1-1 生命周期是什么

生命周期,就是一个引用的有效作用域,从它诞生到它失效的那段范围。大部分时候你不用管它,因为编译器能自动推导——就像类型大部分时候也能自动推导一样。只有当多个引用的关系编译器推不清时,才需要你手动标注。

把类型推导类比过来很好懂:多种类型混在一起时,编译器常要求你标明类型;多个生命周期共存、推导不出关系时,就要求你标明生命周期。

1-2 悬垂引用:生命周期要防的祸

生命周期的核心使命,是避免悬垂引用——也就是引用了已经释放的数据:

{
    let r;
    {
        let x = 5;
        r = &x; // r 引用了 x
    } // x 在这里被释放
    println!("r: {}", r); // r 还指着已消失的 x!
}

r 指向内部块里的 x,可 x 在内部块结尾就被释放了。等外面再打印 r,它引用的东西已经不合法。编译器直接报错:

error[E0597]: `x` does not live long enough

r 活的作用域比 x 大,却引用了活得更短的 x——这就是危险所在。只要反过来,让 xr 活得久,引用就安全。

1-3 借用检查器

Rust 靠一个叫借用检查器的东西,在编译期比对各个引用的生命周期。还是上例,加注释看得更清楚:

{
    let r;                // ---------+-- 'a
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
    println!("r: {}", r); //          |
}                         // ---------+

r 的生命周期是 'ax 的是 'b'b 明显比 'a 小。检查器发现:r 想引用一个活得更短的东西,判定有风险,拒绝编译。这就是生命周期在背后默默保护你。

1-4 函数里的生命周期

来看一个经典函数:返回两个字符串切片里较长那个。

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

逻辑没问题,却编不过:

error[E0106]: missing lifetime specifier

编译器说:返回值是个引用,但签名没说它借用自 x 还是 y。编译器需要知道返回值和参数的关系,才能做借用检查。可这个函数两个分支分别返回 xy,人都不一定说得清返回的是哪个,编译器更不敢猜。这时就得手动标注生命周期。

1-5 生命周期标注语法

生命周期以 ' 开头,通常叫 'a。标注在 & 之后、类型之前:

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

关键认知:生命周期标注不改变任何引用的真实作用域。它只是给编译器看的”契约”,告诉编译器各引用之间该怎么比长短。你标 'a 活得久,该释放时照样释放,不会真的变长。标注是为了”取悦编译器”,让它在借用时放心。

longest 加上标注:

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

和泛型一样,先用 <'a> 声明。这里的意思:对某个生命周期 'axy、返回值都至少活 'a 那么久。实际 'axy 作用域的重合部分——也就是较小的那个。'a 不等于”等于”,而是”大于等于”。

返回值活 'a 久,所以返回值的有效范围不会超过 xy 中较短的那个。这样编译器就能安心了。

1-6 返回引用只能来自参数或本地

函数若返回引用,它的生命周期来源只有两种:函数参数的生命周期,或函数体内新建引用的生命周期

后者就是典型的悬垂引用,必然被拒:

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str() // 返回了函数内本地字符串的引用
}

result 在函数结束就被释放,返回的引用却还指着它,编译器直接报错,从源头杜绝悬垂。解决办法是返回所有权:fn longest(_x: &str, _y: &str) -> String,把 String 交出去。

1-7 结构体里的生命周期

结构体字段也能是引用,但每个引用都得标生命周期:

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

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first = novel.split('.').next().expect("找不到 '.'");
    let i = ImportantExcerpt { part: first };
}

<'a> 声明后,part: &'a str 表示:结构体引用的那段字符串,活得必须不短于结构体本身。上例里 noveli 都活到 main 结束,满足要求。part 标了 'a,结构体才允许持有引用——这也解释了为什么之前结构体字段多用 String:引用太麻烦,不如直接拿所有权。

Warning

若结构体比它引用的数据活得久,编译必报错。比如把 novel 放进内部块、在块外还用 i,就是结构体比字符串长寿,引用失效。

1-8 编译器如何自动消除标注

你平时写的 fn first_word(s: &str) -> &str 没标生命周期却能过,靠的是生命周期消除规则。编译器按三条默认规则推断:

  1. 每个引用参数,自动获得一个独立生命周期fn foo(x: &i32) 视作 fn foo<'a>(x: &'a i32);两个参数就 'a'b 各一个。
  2. 若只有一个输入生命周期,它赋给所有输出生命周期。所以 fn first_word(s: &str) -> &str 自动变成 fn first_word<'a>(s: &'a str) -> &'a str
  3. 若有多个输入生命周期,且其中一个是 &self&mut self,则 &self 的生命周期赋给所有输出。这让方法用起来很顺。

回到 longest(x: &str, y: &str) -> &str:规则 1 给 xy 各一个生命周期;但规则 2 要”只有一个输入”、规则 3 要”有 &self”,都不满足。三条都用不上,编译器推不出返回值该跟谁,于是报错,请你手动标。这就是为什么 longest 必须写 <'a>

1-9 静态生命周期 ‘static

有个特殊生命周期 'static,表示引用能和整个程序活得一样久。字符串字面量就具有 'static,因为它们被硬编码进二进制文件:

let s: &'static str = "我活得久,因为写死在程序里";
Warning

遇到生命周期报错,别图省事给类型强行加 T: 'static'static 能”骗过”编译器,但如果你的引用其实没活那么久,就埋下了隐患。正确思路应先检查:是不是造出了悬垂引用,或生命周期关系本就不一致。

1-10 小结

  • 生命周期是引用的有效作用域,用来防止悬垂引用;
  • 借用检查器在编译期比对生命周期,保证引用指向的数据还活着;
  • 多个引用关系推不清时,用 'a 手动标注,告诉编译器它们怎么关联;
  • 标注不改变真实作用域,只是给编译器的契约;
  • 返回引用只能来自参数(或返回所有权),不能来自函数内本地值;
  • 结构体含引用字段需标生命周期;编译器用三条规则自动消除常见标注;
  • 'static 表示与程序同寿,慎用,别拿它掩盖真实问题。

下一章看三个最常用的标准库 trait:Debug、Display、From。