跳到主要内容

生命周期

问题

什么是生命周期?为什么 Rust 需要生命周期标注?

答案

生命周期(Lifetime)描述了引用保持有效的作用域范围。它的核心目的是防止悬垂引用——确保引用不会比被引用的数据活得更久。

大多数情况下,编译器能自动推断生命周期(就像类型推断一样),但当编译器无法确定引用之间的关系时,需要你手动标注。

生命周期标注不改变引用的实际存活时间

生命周期标注只是告诉编译器多个引用之间的关系(谁必须比谁活得更久),并不会延长或缩短任何引用的实际生命周期。类似于泛型不改变具体类型,而是描述类型之间的约束。

为什么需要生命周期?

考虑这个函数——编译器无法判断返回值的引用来自 x 还是 y

// ❌ 编译错误:missing lifetime specifier
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}

编译器问的是:**返回的引用应该和 x 还是 y 一样长?**如果 x 先被释放,而返回值引用的是 x,就会产生悬垂引用。

生命周期标注语法

'a(单引号 + 小写字母)标注生命周期参数:

// 'a 表示:返回值的引用至少和 x、y 中较短的那个一样长
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}

这里 'a 的含义:

  • x: &'a strx 的引用在 'a 范围内有效
  • y: &'a stry 的引用在 'a 范围内有效
  • 返回 &'a str → 返回值在 'a 范围内有效
  • 'axy 生命周期的交集(较短者)

生命周期的实际约束

fn main() {
let string1 = String::from("long string");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
println!("最长: {}", result); // ✅ result 在 string2 有效期内使用
}
// println!("{}", result); // ❌ string2 已被释放,result 可能引用它
}

编译器根据 'a 推断出 result 的生命周期不能超过 string2,因此在 string2 离开作用域后使用 result 是编译错误。

生命周期省略规则(Lifetime Elision Rules)

Rust 编译器能自动推断大部分场景的生命周期,遵循三条规则:

规则 1:每个引用参数都获得独立的生命周期参数

fn foo(x: &str, y: &str) // → fn foo<'a, 'b>(x: &'a str, y: &'b str)

规则 2:如果只有一个输入生命周期参数,它被赋给所有输出生命周期

fn first_word(s: &str) -> &str  // → fn first_word<'a>(s: &'a str) -> &'a str

规则 3:如果参数中有 &self&mut selfself 的生命周期赋给所有输出生命周期

impl MyStruct {
fn name(&self) -> &str // → fn name<'a>(&'a self) -> &'a str
}

如果三条规则用完,编译器仍无法确定输出生命周期,就会报错,要求手动标注。

结构体中的生命周期

结构体包含引用时必须标注生命周期,确保结构体不会比其引用的数据活得更久:

// 'a 表示 Excerpt 实例的生命周期不能超过 text 引用的数据
struct Excerpt<'a> {
text: &'a str,
}

impl<'a> Excerpt<'a> {
// 规则 3:返回值的生命周期等于 &self
fn level(&self) -> i32 {
3
}

// 需要标注:返回引用可能来自 self.text 或 announcement
fn announce_and_return(&self, announcement: &str) -> &str {
println!("注意: {}", announcement);
self.text // 返回 self.text,所以生命周期跟 self 走
}
}

fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence;
{
let i = novel.split('.').next().unwrap();
first_sentence = Excerpt { text: i };
}
// ✅ novel 还活着,first_sentence.text 引用的是 novel 的数据
println!("{}", first_sentence.text);
}

多个生命周期参数

当引用有不同的生命周期约束时,需要多个生命周期参数:

// 返回值只和 x 的生命周期相关,和 y 无关
fn first_arg<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
println!("y = {}", y);
x // 只返回 x,所以返回值的生命周期只和 'a 相关
}

生命周期子类型(Subtyping)

'a: 'b 表示 'a 至少和 'b 一样长('a'b 的子类型):

fn longest_with_constraint<'a, 'b>(x: &'a str, y: &'b str) -> &'a str
where
'b: 'a, // 'b 至少和 'a 一样长
{
if x.len() > y.len() { x } else { y }
}

'static 生命周期

'static 是最长的生命周期,表示引用在整个程序运行期间都有效:

// 字符串字面量的类型是 &'static str
let s: &'static str = "hello, world";

// const 也是 'static
const MAX_SIZE: usize = 1024;
'static 不等于"永远有效"

'static 意味着数据可以活到程序结束,不代表它一定活到程序结束。例如:

// T: 'static 表示 T 不包含非 'static 引用,T 可能是拥有所有权的类型
fn spawn<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) {
// ...
}

T: 'static 的含义:

  • T 不包含短期引用
  • T 可以是 StringVec<i32> 等拥有所有权的类型(它们满足 'static 约束)
  • 并不要求 T&'static 引用

生命周期与泛型结合

use std::fmt::Display;

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

常见面试问题

Q1: 生命周期标注改变了引用的实际存活时间吗?

答案

不会。生命周期标注只是对编译器的"承诺"——描述多个引用之间的时间关系,帮助编译器验证引用的安全性。它不会延长或缩短任何引用的实际存活时间。

类比:函数签名中的类型标注不会改变变量的值,只是告诉编译器"这个参数是什么类型";生命周期标注类似——告诉编译器"这些引用之间的关系是什么"。

Q2: 生命周期省略规则有哪三条?分别应用在什么场景?

答案

  1. 规则 1:每个引用参数获得独立的生命周期

    • 适用场景:所有含引用参数的函数
    • fn foo(x: &str, y: &str)fn foo<'a, 'b>(x: &'a str, y: &'b str)
  2. 规则 2:只有一个输入生命周期时,赋给所有输出

    • 适用场景:单参数引用函数
    • fn first(s: &str) -> &strfn first<'a>(s: &'a str) -> &'a str
  3. 规则 3:有 &self/&mut self 时,self 的生命周期赋给输出

    • 适用场景:方法(impl 块中的函数)
    • fn name(&self) -> &strfn name<'a>(&'a self) -> &'a str

三条规则依次应用,如果都用完还无法确定输出生命周期,编译器报错。

Q3: 'static 生命周期有哪些常见用途?

答案

场景示例说明
字符串字面量"hello"&'static str编译进二进制,程序运行期间有效
全局常量const N: i32 = 42;编译期确定,全局有效
线程 boundT: Send + 'static线程可能比创建者活得更久,需保证数据有效
Trait ObjectBox<dyn Error + 'static>无非 static 引用,可以安全传递
lazy_static! / OnceCell延迟初始化的全局数据运行时初始化,全局有效

Q4: T: 'static&'static T 有什么区别?

答案

这是非常容易混淆的概念:

  • &'static T:一个指向 T 的引用,这个引用在整个程序运行期间都有效。例如字符串字面量 "hello" 的类型是 &'static str

  • T: 'staticT 类型本身不包含任何非 'static 的引用。换言之,T 要么不含引用、要么只含 'static 引用

// T: 'static 可以是拥有所有权的类型
fn foo<T: 'static>(x: T) {}

foo(String::from("hello")); // ✅ String 不含引用,满足 'static
foo(42_i32); // ✅ i32 不含引用,满足 'static
foo("hello"); // ✅ &'static str 满足 'static

let local = String::from("hi");
// foo(&local); // ❌ &String(非 'static 生命周期)

Q5: 什么时候必须手动标注生命周期?

答案

省略规则不够用时需要手动标注,主要场景:

  1. 多个引用参数,返回引用——编译器不知道返回值和哪个参数关联:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { ... }
  1. 结构体包含引用字段
struct Config<'a> {
name: &'a str,
}
  1. impl 块中有复杂引用关系
impl<'a> Config<'a> {
fn update(&mut self, name: &'a str) {
self.name = name;
}
}
  1. Trait Object 需要明确生命周期
fn returns_trait<'a>(s: &'a str) -> Box<dyn Iterator<Item = &'a str> + 'a> {
Box::new(s.split_whitespace())
}

Q6: 如何理解高阶生命周期(HRTB)for<'a>

答案

HRTB(Higher-Ranked Trait Bounds)用于表达"对所有可能的生命周期 'a"的约束,最常见于闭包和函数指针:

// 普通生命周期:'a 是调用方指定的某个具体生命周期
fn apply<'a>(f: fn(&'a str) -> &'a str, s: &'a str) -> &'a str {
f(s)
}

// HRTB:f 能接受任意生命周期的引用
fn apply_hrtb(f: for<'a> fn(&'a str) -> &'a str, s: &str) -> &str {
f(s)
}

for<'a> 的含义是"对于任意选择的生命周期 'a",表示函数不挑剔引用的具体生命周期。

闭包 trait bound 中最常见:

// Fn(&str) -> &str 实际上是 for<'a> Fn(&'a str) -> &'a str 的语法糖
fn apply_closure(f: impl Fn(&str) -> &str) -> String {
f("hello").to_string()
}

Q7: 生命周期和 NLL 是什么关系?

答案

NLL(Non-Lexical Lifetimes)改进了生命周期的作用域推断方式:

方面旧模型(Lexical)NLL
生命周期结束点花括号结束最后一次使用
分析方式基于词法作用域基于控制流图(CFG)
用户体验经常误报,需额外 {}大幅减少误报

NLL 不改变生命周期的概念,只改变了编译器推断生命周期结束点的方式,让借用检查更精准。

详细内容请参阅 借用与引用

Q8: 生命周期标注在 async 代码中有什么陷阱?

答案

async 函数中引用的生命周期特别容易出问题,因为 async fn 会生成一个 Future,引用必须在 Future 整个生命周期内有效:

// ❌ 常见错误:借用局部变量后 await
async fn process(data: &str) -> String {
data.to_uppercase()
}

async fn problematic() {
let s = String::from("hello");
let result = process(&s).await; // ✅ 只要 s 在 await 期间存活
println!("{}", result);
}

常见陷阱:

  1. 跨 await 借用:引用必须在所有 .await 点之间保持有效
  2. Send bound&T 跨 await 时,T 必须是 Sync(因为 Future 可能被调度到另一个线程)
  3. 解决方案:在 await 前 clone 或将引用转为 owned 数据

更多内容请参阅 异步代码调试

相关链接