生命周期
这篇讲生命周期(Lifetime)——Rust 编译器用来确保所有引用在其指向的数据有效期间内不会失效的机制。所有权规定了「谁拥有」,生命周期规定了「能用多久」。
生命周期解决了什么
看一个编译器能发现的错误:
fn main() {
let r;
{
let x = 5;
r = &x; // ❌ x 活得不够久
} // x 被释放
println!("{r}"); // r 指向已释放的内存——编译器拒绝!
}编译器报错:「x does not live long enough」。生命周期就是编译器用来追踪这种「存活时间」关系的一套标注系统。
生命周期标注语法
生命周期标注用 'a(单引号 + 小写字母)表示,写在引用的 & 之后:
&i32 // 普通引用
&'a i32 // 带有生命周期标注的引用
&'a mut i32 // 带有生命周期标注的可变引用
关键理解:生命周期标注不改变引用的实际存活时间,它只是告诉编译器「这几个引用的生命周期之间存在什么关系」。标注本身不影响运行时代码。
函数签名中的生命周期
当函数接受多个引用作为参数并返回引用时,编译器无法自动推断返回值该和哪个参数「绑定」:
// ❌ 编译失败——返回值引用的生命周期来自哪里?
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() { x } else { y }
}需要手动标注:
// ✅ 标注了生命周期:返回值与参数 x、y 中较短的那个活得一样久
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String::from("abcd");
{
let s2 = String::from("xyz");
let result = longest(s1.as_str(), s2.as_str());
println!("最长的是: {}", result); // ✅
} // s2 被释放
}'a 的实际含义:x 和 y 的生命周期至少与 'a 一样长,返回的引用在 'a 内有效。
结构体中的生命周期
如果结构体包含引用字段,必须标注生命周期:
// 表示:ImportantExcerpt 的实例不能比它持有的 part 引用活得更久
struct ImportantExcerpt<'a> {
part: &'a str, // part 是一个字符串引用
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = ImportantExcerpt {
part: first_sentence,
};
// excerpt 不能比 novel 活得更久
}生命周期省略规则
Rust 编译器在大多数情况下能自动推断生命周期,无需手动标注。它依据三条规则:
- 每个引用参数都有自己的生命周期。
fn foo(x: &str)→fn foo<'a>(x: &'a str) - 如果只有一个输入生命周期,它被赋给所有输出生命周期。
fn foo(x: &str) -> &str→fn foo<'a>(x: &'a str) -> &'a str - 如果方法有
&self或&mut self参数,其生命周期被赋给所有输出生命周期
这就是为什么很多简单函数不需要手动标注——省略规则覆盖了大多数场景。
// 省略规则自动处理,无需标注
fn first_word(s: &str) -> &str { // 规则 1 + 2 自动推断
&s[..s.find(' ').unwrap_or(s.len())]
}
// 但有两个输入引用时,规则不够用——需要手动标注
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { ... }静态生命周期
'static 是一个特殊的生命周期,表示引用在整个程序运行期间都有效:
let s: &'static str = "hello world"; // 字符串字面量存在二进制只读区,天然是 'static
不要看到生命周期报错就给所有东西加
'static——这是新手常见的反模式。大多数情况下你只需要正确标注参数之间的关系,而不是让东西永远存活。一句话小结
生命周期是编译器确保引用不会「悬垂」的标注系统,不改变运行时行为。大多数场景有省略规则自动推导,只有函数的多个引用参数有复杂返回关系时才需要手动标注。编译器越「唠叨」,运行时越安全。下一篇讲 结构体与枚举。
练习
- 下面代码中
longest的调用为什么能通过编译?
fn main() {
let s1 = String::from("long string is long");
let result;
{
let s2 = String::from("xyz");
result = longest(s1.as_str(), s2.as_str());
} // s2 被释放
// println!("{result}"); // ❌ 如果取消注释呢?
}- 为下面的结构体和方法补全生命周期标注:
struct StrWrap {
data: &str,
}
impl StrWrap {
fn get(&self) -> &str {
self.data
}
}参考答案
result = longest(...)可以编译——Rust 只检查引用的生命周期是否够用,s1和s2在赋值时都有效,所以可以调用。但如果取消注释println!("{result}"),编译器会报错——result的生命周期受s2约束,而s2在该行之前已被释放。标注后:
struct StrWrap<'a> {
data: &'a str,
}
impl<'a> StrWrap<'a> {
fn get(&self) -> &str { // 省略规则 3 自动处理返回值生命周期
self.data
}
}最后更新于