第3章 - 生命周期
嗨,朋友!我是长安。
欢迎来到进阶教程最有挑战性的一章——生命周期(Lifetimes)!
老实说,生命周期是 Rust 中最难理解的概念之一。但别担心,我会用最简单的方式给你讲清楚。理解了生命周期,你对 Rust 的理解就会上一个新台阶!
🤔 什么是生命周期?
还记得我们学过的引用吗?引用就像是借别人的东西用一下。
生命周期就是告诉 Rust 编译器:"这个引用可以活多久"。
想象一下,你借了朋友的一本书:
- 如果朋友还没还书就搬家了,那这本书就不存在了,你的借书卡就无效了
- 这就是悬垂引用(dangling reference)的问题
Rust 的生命周期系统就是为了在编译时防止这种情况!
长安说
生命周期不是一个新的运行时特性,而是编译器用来验证你的代码是否安全的工具。
大多数情况下,Rust 能自动推断生命周期,你不需要显式标注。只有在编译器无法确定时,才需要你手动标注。
💔 悬垂引用问题
先看一个会出错的例子:
fn main() {
let r;
{
let x = 5;
r = &x; // ❌ 错误!x 即将离开作用域
} // x 在这里被清理
println!("r: {}", r); // r 引用的 x 已经不存在了!
}
编译器会报错:
error[E0597]: `x` does not live long enough
--> src/main.rs:6:13
|
6 | r = &x;
| ^^ borrowed value does not live long enough
7 | }
| - `x` dropped here while still borrowed
8 |
9 | println!("r: {}", r);
| - borrow later used here
编译器说:"x 活得不够久"!
📝 生命周期标注语法
生命周期参数用单引号 ' 开头,通常用小写字母,最常用的是 'a:
&i32 // 引用
&'a i32 // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用
注意
'a 读作 "tick a" 或 "lifetime a"。它只是一个名字,你也可以用 'b、'c 或任何有意义的名字,比如 'input、'output。
🔗 函数中的生命周期
为什么需要标注?
看这个函数:
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 string1 = String::from("long string");
let string2 = String::from("short");
let result = longest(&string1, &string2);
println!("最长的字符串: {}", result);
}
这里 <'a> 声明了一个生命周期参数。这个标注的意思是:
"返回的引用的生命周期与
x和y中生命周期较短的那个一致。"
长安说
把 <'a> 理解为一个“合同”:
你告诉编译器:“参数 x 和 y 的生命周期至少要和返回值的生命周期一样长。”
生命周期有多长?
fn main() {
let string1 = String::from("long string");
{
let string2 = String::from("short");
let result = longest(&string1, &string2);
println!("最长: {}", result); // ✅ OK
} // string2 在这里被清理
}
但这样不行:
fn main() {
let string1 = String::from("long string");
let result;
{
let string2 = String::from("short");
result = longest(&string1, &string2);
} // ❌ string2 被清理,但 result 可能引用它
println!("result: {}", result); // ❌ 错误!
}
因为 result 的甛命周期受到较短的 string2 限制,不能比 string2 活得更久。
🏗️ 结构体中的生命周期
如果结构体包含引用,必须标注生命周期:
struct Book<'a> {
title: &'a str,
author: &'a str,
}
fn main() {
let title = String::from("《Rust 编程》");
let author = String::from("长安");
let book = Book {
title: &title,
author: &author,
};
println!("{} by {}", book.title, book.author);
} // book、title、author 同时离开作用域
Book<'a> 表示:“Book 实例不能比它引用的数据活得更久。”
实例:文章预览
struct Article<'a> {
content: &'a str,
}
impl<'a> Article<'a> {
fn preview(&self, len: usize) -> &str {
if self.content.len() <= len {
self.content
} else {
&self.content[..len]
}
}
}
fn main() {
let text = String::from("这是一篇很长的文章...");
let article = Article { content: &text };
println!("预览: {}", article.preview(10));
}
🧐 生命周期省略规则
Rust 有三个生命周期省略规则,让你在大多数情况下不需要显式标注:
规刱1:每个引用参数都有自己的生命周期
fn first_word(s: &str) -> &str { // 自动推断为 fn first_word<'a>(s: &'a str) -> &str
// ...
}
规刱2:如果只有一个引用参数,返回值的生命周期与它相同
fn first_word(s: &str) -> &str { // 自动推断为 fn first_word<'a>(s: &'a str) -> &'a str
s.split_whitespace().next().unwrap_or("")
}
规刱3:如果是方法,返回值的生命周期与 self 相同
impl<'a> Book<'a> {
fn get_title(&self) -> &str { // 自动推断为 &'a str
self.title
}
}
长安说
这就是为什么你写了很多 Rust 代码都没遇到生命周期标注——因为编译器已经能自动推断了!
🔍 多个生命周期
有时候需要多个不同的生命周期:
fn longest_with_announcement<'a, 'b>(
x: &'a str,
y: &'a str,
ann: &'b str,
) -> &'a str {
println!("通知: {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
这里 'a 和 'b 是独立的生命周期,返回值只依赖于 'a。
🎉 静态生命周期
'static 是一个特殊的生命周期,表示数据在整个程序运行期间都有效:
let s: &'static str = "我是静态字符串"; // 存储在程序二进制文件中
static HELLO: &str = "Hello, world!"; // 全局静态变量
注意
不要滥用 'static!大多数情况下,编译器提示你使用 'static 是因为你的代码有其他问题,不是真的需要静态生命周期。
🎯 实战例子:字符串分割器
struct StrSplit<'a> {
remainder: &'a str,
delimiter: &'a str,
}
impl<'a> StrSplit<'a> {
fn new(haystack: &'a str, delimiter: &'a str) -> Self {
StrSplit {
remainder: haystack,
delimiter,
}
}
}
impl<'a> Iterator for StrSplit<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
if let Some(next_delim) = self.remainder.find(self.delimiter) {
let until_delim = &self.remainder[..next_delim];
self.remainder = &self.remainder[next_delim + self.delimiter.len()..];
Some(until_delim)
} else if !self.remainder.is_empty() {
let rest = self.remainder;
self.remainder = "";
Some(rest)
} else {
None
}
}
}
fn main() {
let text = "hello::world::rust";
let split = StrSplit::new(text, "::");
for word in split {
println!("{}", word);
}
}
💡 小结
在这一章,我们学会了:
- 生命周期确保引用总是有效的
- 使用
'a语法标注生命周期 - 函数中的生命周期:连接参数和返回值
- 结构体中的生命周期:确保结构体不比引用活得更久
- 生命周期省略规则:大多数情况下自动推断
'static生命周期:整个程序运行期间
核心要点:
- 生命周期是编译时检查,不影响运行时性能
<'a>是泛型参数,和<T>类似- 生命周期不改变引用的长度,只是描述关系
- 大多数情况编译器能自动推断
长安的建议
生命周期确实很难,但不用担心:
- 大多数时候不需要:编译器能自动推断
- 遇到错误再学:等编译器提示你加生命周期时,再仔细理解
- 重点是理解原理:为什么需要生命周期(防止悬垂引用)
- 多写代码:理论不如实践,多写就会了
记住:生命周期标注不是在改变引用的生命周期,而是在告诉编译器不同引用之间的关系!
🚀 下一步
恭喜你掌握了生命周期!这是 Rust 最难的部分之一,能学到这里已经非常不容易了!
下一章,我们会学习 智能指针,它们是 Rust 中一些有特殊能力的指针类型。
💪 练习题
- 修复这个函数,添加必要的生命周期标注:
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
定义一个结构体
ImportantExcerpt,存储一个字符串引用,并为它实现一个方法返回这个引用写一个函数,接受两个字符串引用和一个通知字符串,返回较短的字符串
答案示例
// 练习1 - 实际上不需要标注,编译器自动推断
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
// 如果显式标注:
fn first_word<'a>(s: &'a str) -> &'a str {
s.split_whitespace().next().unwrap_or("")
}
// 练习2
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn get_part(&self) -> &str {
self.part
}
}
// 练习3
fn shorter<'a, 'b>(x: &'a str, y: &'a str, ann: &'b str) -> &'a str {
println!("通知: {}", ann);
if x.len() < y.len() {
x
} else {
y
}
}
fn main() {
let excerpt = ImportantExcerpt {
part: "Call me Ishmael.",
};
println!("{}", excerpt.get_part());
let s1 = "hello";
let s2 = "world!";
let result = shorter(s1, s2, "Comparing");
println!("Shorter: {}", result);
}
