Rust: Rust 生命周期(Lifetimes):引用的有效期标注
最后更新:2026-08-26
生命周期(Lifetime)是 Rust 确保"引用永远不会悬垂"的机制——它给每个引用贴上了"有效期"标签。
生命周期你其实已经在用了——第 9 课和第 10 课的所有代码都有生命周期,只是编译器帮你"猜"出来了。本课教你什么时候需要手动标注生命周期。
1. 你将学到
- 为什么需要生命周期——防止悬垂引用
- 生命周期标注语法:
'a、'b - 函数签名中的生命周期——连接参数和返回值的生命周期
- 生命周期省略规则——编译器何时能自动推断
- 结构体中的生命周期标注
'static生命周期——永久的引用
2. 概念图解
flowchart LR
subgraph "函数签名 fn longest<'a>"
X["x: &'a str"] --> RET["返回值: &'a str"]
Y["y: &'a str"] --> RET
end
NOTE["'a = x 和 y 中较短者的生命周期"] -.-> RET
3. 一个追踪老鼠的故事
(1) 痛苦:老鼠跑了还在看
Tom 是一名生物学家,他在用一个追踪器记录老鼠的位置:
追踪器锁定老鼠 A → 老鼠 A 跑进了洞里 → 追踪器还在指向洞口
- 追踪器(引用)指向老鼠 A(数据)
- 老鼠 A 跑了(数据被销毁)
- 追踪器还在指向那个位置(悬垂引用——危险!)
"如果追踪器上有个标签写着'有效期限:到老鼠 A 出洞为止'就好了……"
(2) Rust 生命周期的解决方案
// 生命周期标注 'a:告诉编译器"返回值的有效期不超过参数 x 的有效期"
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let string1 = String::from("长字符串");
let result;
{
let string2 = String::from("短");
result = longest(&string1, &string2); // ✅ string2 和 string1 都还活着
println!("较长的是: {}", result);
} // string2 在这里被销毁
// println!("{}", result); // ❌ 如果在这里打印,result 引用的 string2 已销毁
} // string1 在这里被销毁
生命周期标注就像给追踪器贴了一个"有效期"标签。编译器根据这个标签判断引用是否还在有效期内。在这个例子中,
result的生命周期不能超过string2的生命周期。
4. 生命周期工作原理
(1) 每个引用都有生命周期
fn main() {
let x: i32 = 10; // x 的生命周期从这里开始
let r: &i32 = &x; // r 的生命周期从这里开始
println!("r: {}", r); // 使用 r
} // r 的生命周期先结束,然后 x 的生命周期结束
(2) 为什么需要标注生命周期?
当函数返回一个引用时,编译器需要知道这个引用来自哪个参数:
graph TB
A[函数返回引用] --> B{返回值指向哪个参数?}
B --> C[指向参数1 → 返回值的生命周期 ≤ 参数1]
B --> D[指向参数2 → 返回值的生命周期 ≤ 参数2]
B --> E[指向两个中的某一个 → 必须标注: 取交集]
C --> F[无需标注(编译器能推断)]
D --> F
E --> G[需要手动标注 'a]
| 场景 | 例子 | 是否需要标注 |
|---|---|---|
| 单个输入引用 | fn first(x: &str) -> &str |
不需要(省略规则) |
| 多个输入引用 | fn longest(x: &str, y: &str) -> &str |
需要(编译器不知选谁) |
| 不返回引用 | fn len(x: &str) -> usize |
不需要 |
| 结构体含引用 | struct S<'a> { r: &'a i32 } |
需要 |
(3) 生命周期标注位置速查
| 位置 | 语法 | 说明 |
|---|---|---|
| 函数定义 | fn foo<'a>(x: &'a str) -> &'a str |
连接参数和返回值 |
| 结构体定义 | struct S<'a> { r: &'a str } |
结构体不能比引用活得更久 |
| impl 块 | impl<'a> S<'a> |
实现方法时声明生命周期 |
| trait 约束 | where T: 'a |
约束类型不能含短命引用 |
| 静态标注 | &'static str |
整个程序运行期间有效 |
5. 生命周期标注
(1) 语法
// 'a 是生命周期参数的名称(习惯用 'a、'b、'c)
// 读法:对于生命周期 'a,x 和 y 都至少需要活 'a 这么久
fn function<'a>(x: &'a str, y: &'a str) -> &'a str {
// 返回值的生命周期 = x 和 y 中较短的那个
}
(2) 生命周期省略规则
编译器在以下三种情况下可以自动推断生命周期(不需要你写):
| 规则 | 含义 |
|---|---|
| 每个输入引用都有各自的生命周期 | fn foo(x: &str) 自动给 x 一个生命周期 |
| 只有一个输入引用 | 输出引用的生命周期 = 输入引用的生命周期 |
| 多个输入,其中一个是 &self 或 &mut self | 输出引用生命周期 = &self 的生命周期 |
// 不需要标注:只有一个输入引用
fn first_word(s: &str) -> &str { &s[..] }
// 需要标注:两个输入引用
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
6. 生命周期示例
▶ 示例 1:为什么需要生命周期(难度 ⭐⭐⭐)
// ============================================
// 没有生命周期标注会怎样?
// ============================================
// 这个函数有两个输入引用,返回值可能是其中任何一个
// ❌ 没有标注会编译报错:expected lifetime parameter
// fn longest_wrong(x: &str, y: &str) -> &str {
// if x.len() > y.len() { x } else { 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("hello");
let result;
{
let s2 = String::from("hi");
result = longest(&s1, &s2); // result 的生命周期 = s2 的生命周期(较短的那个)
println!("较长的是: {}", result);
} // s2 被销毁,result 无效
// println!("{}", result); // ❌ 编译器会阻止:result 的生命周期已经结束
}
输出:
较长的是: hello
生命周期
'a取的是x和y中较短的那个。如果返回值的生命周期设为长的那一个,它可能在短的那个销毁后仍然被使用——这就是编译器要防止的。
▶ 示例 2:不同生命周期的参数(难度 ⭐⭐⭐)
// ============================================
// 两个参数有不同生命周期
// ============================================
// 两个生命周期:'a 对应 x,'b 对应 y
// 返回值只与 x 有关,所以用 'a
fn choose_first<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
x // 只返回 x,所以生命周期只需要 'a
}
fn main() {
let x = String::from("长命百岁");
let result;
{
let y = String::from("短命鬼");
result = choose_first(&x, &y); // ✅ result 只依赖于 x
println!("result: {}", result);
} // y 销毁,但 result 不依赖 y,所以没问题
println!("离开内层后仍然有效: {}", result); // ✅ x 还活着
}
输出:
result: 长命百岁
离开内层后仍然有效: 长命百岁
当返回值只依赖于某个参数时,只需标注该参数的生命周期。
choose_first的y参数即使短命也不影响结果——因为返回值根本没用到y的数据。
▶ 示例 3:结构体中的生命周期(难度 ⭐⭐⭐⭐)
// ============================================
// 结构体里存放引用——必须标注生命周期
// ============================================
struct Excerpt<'a> {
content: &'a str, // 结构体借用外部的字符串
}
impl<'a> Excerpt<'a> {
fn length(&self) -> usize {
self.content.len()
}
fn announce(&self, announcement: &str) -> &str {
println!("公告: {}", announcement);
self.content // 返回结构体内部的引用
}
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("找不到句号");
let excerpt = Excerpt {
content: &first_sentence,
};
println!("摘录内容: {}", excerpt.content);
println!("长度: {}", excerpt.length());
println!("公告结果: {}", excerpt.announce("欢迎阅读"));
} // excerpt 必须先于 novel 销毁(内容依赖 novel)
输出:
摘录内容: Call me Ishmael
长度: 15
公告结果: Call me Ishmael
结构体里存放引用时,必须在结构体名字上标注生命周期参数
<'a>。这告诉编译器:结构体实例的生命周期不能超过它引用的数据的生命周期。
▶ 示例 4:'static 生命周期(难度 ⭐⭐⭐)
// ============================================
// 'static:引用在整个程序运行期间有效
// ============================================
fn main() {
// 字符串字面量是 'static 的
let s: &'static str = "我活在程序的整个生命周期里";
println!("{}", s);
// 'static 约束:要求 T 不能包含任何非静态引用
fn print_static<T: 'static>(val: &T) {
println!("{:?}", std::ptr::from_ref(val));
}
let num: i32 = 42;
print_static(&num); // ✅ i32 不包含引用,符合 'static
// let msg = String::from("temp");
// let r = &msg;
// print_static(&r); // ❌ r 引用了非 'static 的 msg
}
输出:
我活在程序的整个生命周期里
0x...(内存地址)
'static是 Rust 中最长的生命周期——数据在程序启动时就存在,直到程序结束才销毁。字符串字面量就是'static的。请注意:'static应该被视为"这个引用永远有效",而不是"这个数据一直活到最后"。
▶ 示例 5:综合练习——文本分析器(难度 ⭐⭐⭐)
// ============================================
// 生命周期实战:零拷贝文本分析
// ============================================
struct TextAnalyzer<'a> {
text: &'a str,
}
impl<'a> TextAnalyzer<'a> {
fn new(text: &'a str) -> Self {
TextAnalyzer { text }
}
fn word_count(&self) -> usize {
self.text.split_whitespace().count()
}
fn longest_word(&self) -> &'a str {
self.text
.split_whitespace()
.max_by_key(|w| w.len())
.unwrap_or("")
}
fn first_n_words(&self, n: usize) -> Vec<&'a str> {
self.text.split_whitespace().take(n).collect()
}
fn line_count(&self) -> usize {
self.text.lines().count()
}
fn char_count(&self) -> usize {
self.text.chars().count()
}
fn summary(&self) -> String {
format!(
"字符: {}, 单词: {}, 行数: {}, 最长词: '{}'",
self.char_count(),
self.word_count(),
self.line_count(),
self.longest_word()
)
}
}
fn highlight_word<'a>(text: &'a str, word: &str) -> Vec<&'a str> {
text.split_whitespace()
.filter(|w| w.contains(word))
.collect()
}
fn main() {
let article = "Rust is a systems programming language \
that runs blazingly fast and prevents segfaults. \
Rust guarantees memory safety and thread safety.";
let analyzer = TextAnalyzer::new(article);
println!("=== 文本分析 ===");
println!("{}", analyzer.summary());
let top3 = analyzer.first_n_words(3);
println!("\n前 3 个词: {:?}", top3);
let rust_mentions = highlight_word(article, "Rust");
println!("包含 'Rust' 的词: {:?}", rust_mentions);
let paragraph = "First line.\nSecond line.\nThird line.";
let p_analyzer = TextAnalyzer::new(paragraph);
println!("\n段落分析: {}", p_analyzer.summary());
}
输出:
=== 文本分析 ===
字符: 124, 单词: 18, 行数: 1, 最长词: 'blazingly'
前 3 个词: ["Rust", "is", "a"]
包含 'Rust' 的词: ["Rust", "Rust"]
段落分析: 字符: 39, 单词: 6, 行数: 3, 最长词: 'Second'
TextAnalyzer通过生命周期<'a>持有对原始文本的引用——所有分析方法都是零拷贝的。longest_word返回的&'a str指向原始文本中的单词切片,不会产生任何内存分配。
❓ 常见问题
📖 小节
- 生命周期是 Rust 确保引用有效的编译时机制
- 用
'a标注语法连接输入引用和输出引用的有效期 - 生命周期省略规则让 80%+ 的场景不需要手动标注
- 结构体里存引用必须标注生命周期参数
'static是最长的生命周期(整个程序运行期间)- 生命周期不产生运行时开销——编译完就消失了
📝 作业
- 难度 ⭐:阅读一段已有的 Rust 代码(比如第 9 课的例程),找出哪些函数靠生命周期省略规则避免了手动标注。
- 难度 ⭐⭐⭐:写一个函数
fn shortest<'a>(x: &'a str, y: &'a str) -> &'a str,返回两个字符串中较短的那个。在main中创建不同生命周期的变量来调用它。 - 难度 ⭐⭐⭐⭐:定义一个结构体
Book<'a>包含&'a str类型的字段title。写一个impl块,实现一个方法fn first_word(&self) -> &str返回标题的第一个单词。在main中演示它的使用。