第九章:生命周期深入——引用的守护者
本章导读:生命周期是 Rust 最令人生畏的概念,但也是最精妙的设计之一。它解决了编程中一个根本问题:如何确保引用始终指向有效的数据?本章将深入生命周期的本质,揭示它不是神秘的魔法,而是一套优雅的规则系统。理解了生命周期,你就真正理解了 Rust。
🤔 9.1 为什么需要生命周期?
💥 9.1.1 悬垂引用问题
考虑这个 C 代码:
int* bad_function() {
int x = 10;
return &x; // 返回局部变量的指针!
} // x 在这里被销毁
调用者得到一个指向已释放内存的指针——这是未定义行为,可能导致崩溃或安全漏洞。
🛡️ 9.1.2 Rust 的解决方案
Rust 在编译时检测这类问题:
fn bad_function() -> &i32 {
let x = 10;
&x // 错误:`x` 在借用时已离开作用域
}
编译器的错误信息:
error[E0515]: cannot return reference to local variable `x`
--> src/lib.rs:2:5
|
2 | &x
| ^^ returns a reference to data owned by the current function
生命周期系统的工作就是确保引用不会比它指向的数据活得更长。
📐 9.2 生命周期标注语法
🔤 9.2.1 基本语法
生命周期标注使用 ' 加上名称:
&i32 // 没有标注的生命周期(编译器推断)
&'a i32 // 有显式生命周期的引用
&'a mut i32 // 有显式生命周期的可变引用
重要:生命周期标注不会改变引用实际存活的时间!它们只是描述引用之间的关系,帮助编译器验证代码的安全性。
📝 9.2.2 函数中的生命周期
当函数接受和返回引用时,编译器需要知道它们之间的关系:
// 编译失败!编译器不知道返回值的生命周期
// fn longest(x: &str, y: &str) -> &str {
// 正确:告诉编译器返回值与参数有相同的生命周期
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
这里的 'a 表示:参数 x、y 和返回值都至少存活 'a 这么长。
fn main() {
let string1 = String::from("long string");
{
let string2 = String::from("xyz");
let result = longest(&string1, &string2);
println!("最长的: {}", result); // 正确
}
// 编译失败示例
// let result;
// {
// let string2 = String::from("xyz");
// result = longest(&string1, &string2);
// }
// println!("{}", result); // 错误!string2 已被释放
}
🏗️ 9.2.3 结构体中的生命周期
当结构体包含引用时,必须标注生命周期:
struct Book<'a> {
title: &'a str,
author: &'a str,
}
impl<'a> Book<'a> {
fn new(title: &'a str, author: &'a str) -> Self {
Self { title, author }
}
fn summary(&self) -> String {
format!("《{}》 - {}", self.title, self.author)
}
}
fn main() {
let title = String::from("Rust 程序设计语言");
let author = String::from("Steve Klabnik");
let book = Book::new(&title, &author);
println!("{}", book.summary());
// book 不能比 title 和 author 活得更长
}
🔧 9.3 生命周期省略规则
好消息是:不是每个函数都需要显式标注生命周期。Rust 有三套省略规则。
📜 9.3.1 三条规则
规则一:每个引用参数都获得自己的生命周期参数。
fn foo(x: &str, y: &str)
// 等价于
fn foo<'a, 'b>(x: &'a str, y: &'b str)
规则二:如果只有一个输入生命周期参数,它被赋予所有输出生命周期参数。
fn foo(x: &str) -> &str
// 等价于
fn foo<'a>(x: &'a str) -> &'a str
规则三:如果有多个输入生命周期参数,但其中一个是 &self 或 &mut self,self 的生命周期被赋予所有输出生命周期参数。
impl MyStruct {
fn foo(&self, x: &str) -> &str
// 等价于
fn foo<'a, 'b>(&'a self, x: &'b str) -> &'a str
}
✅ 9.3.2 省略规则应用示例
// 可以省略(规则二)
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
// 必须显式(多个输入引用,返回其中一个)
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// 可以省略(规则三)
impl Parser {
fn parse(&self, input: &str) -> Result<&str, Error> {
// 返回值生命周期与 self 相同
}
}
🔗 9.4 高级生命周期概念
🎯 9.4.1 生命周期子类型
生命周期可以有包含关系:'big: 'small 表示 'big 至少和 'small 一样长。
fn choose_first<'a, 'b: 'a>(first: &'a str, _second: &'b str) -> &'a str {
first
}
// 实际上,更简单的写法也行
fn choose_first_simple<'a>(first: &'a str, _second: &str) -> &'a str {
first
}
📦 9.4.2 生命周期边界
可以在泛型参数上添加生命周期约束:
struct RefWrapper<'a, T: 'a> {
reference: &'a T,
}
// T: 'a 意味着 T 可以安全地被引用 'a 这么长
// 对于大多数类型,这是自动满足的
// 但如果 T 包含引用,那些引用必须至少存活 'a
🔄 9.4.3 高阶生命周期(HRTB)
高阶 Trait 约束(Higher-Ranked Trait Bounds)用于复杂场景:
fn apply_to_all<F>(data: &[i32], f: F) -> Vec<i32>
where
F: for<'a> Fn(&'a i32) -> i32, // 对所有生命周期 'a 都成立
{
data.iter().map(f).collect()
}
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
let doubled = apply_to_all(&numbers, |&x| x * 2);
println!("{:?}", doubled);
}
for<'a> 表示闭包必须能接受任意生命周期的引用。
🏰 9.4.4 '_ 匿名生命周期
当编译器能推断时,可以用 '_ 省略:
struct StrWrapper<'a> {
s: &'a str,
}
fn make_wrapper(s: &str) -> StrWrapper<'_> {
StrWrapper { s }
}
fn print_wrapper(w: &StrWrapper<'_>) {
println!("{}", w.s);
}
⚔️ 9.5 常见生命周期模式
🔄 9.5.1 多个不同的生命周期
// 返回值与第一个参数同生命周期
fn first<'a, 'b>(x: &'a str, _y: &'b str) -> &'a str {
x
}
// 返回值与第二个参数同生命周期
fn second<'a, 'b>(_x: &'a str, y: &'b str) -> &'b str {
y
}
// 返回值与两者都有关(取交集)
fn longer<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
📚 9.5.2 结构体持有多个引用
struct Context<'a, 'b> {
input: &'a str,
config: &'b str,
}
impl<'a, 'b> Context<'a, 'b> {
fn new(input: &'a str, config: &'b str) -> Self {
Self { input, config }
}
// 返回值可以选择与任一引用同生命周期
fn input(&self) -> &'a str {
self.input
}
fn config(&self) -> &'b str {
self.config
}
}
🧵 9.5.3 静态生命周期
'static 表示引用在程序整个运行期间有效:
// 字符串字面量是 'static 的
let s: &'static str = "Hello, world!";
// 静态变量
static GREETING: &str = "Hello!";
// 函数签名
fn get_static() -> &'static str {
"I live forever!"
}
// 警告:不要轻易用 'static 解决生命周期问题
// 错误示例
fn bad_return<'a>() -> &'a str {
"This works but is confusing" // 实际上是 'static
}
// 更清晰
fn good_return() -> &'static str {
"This is explicit"
}
🧪 9.6 实战:构建文本解析器
让我们构建一个需要仔细考虑生命周期的文本解析器:
#[derive(Debug)]
pub struct Token<'a> {
pub kind: TokenKind,
pub text: &'a str,
pub line: usize,
pub column: usize,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TokenKind {
Identifier,
Number,
String,
Operator,
Keyword,
Whitespace,
Newline,
Unknown,
}
pub struct Lexer<'a> {
input: &'a str,
chars: std::iter::Peekable<std::str::Chars<'a>>,
position: usize,
line: usize,
column: usize,
}
impl<'a> Lexer<'a> {
pub fn new(input: &'a str) -> Self {
Self {
input,
chars: input.chars().peekable(),
position: 0,
line: 1,
column: 1,
}
}
pub fn peek(&mut self) -> Option<char> {
self.chars.peek().copied()
}
pub fn advance(&mut self) -> Option<char> {
let ch = self.chars.next()?;
self.position += ch.len_utf8();
if ch == '\n' {
self.line += 1;
self.column = 1;
} else {
self.column += 1;
}
Some(ch)
}
fn skip_whitespace(&mut self) {
while let Some(ch) = self.peek() {
if ch.is_whitespace() && ch != '\n' {
self.advance();
} else {
break;
}
}
}
fn read_identifier(&mut self) -> &'a str {
let start = self.position;
while let Some(ch) = self.peek() {
if ch.is_alphanumeric() || ch == '_' {
self.advance();
} else {
break;
}
}
&self.input[start..self.position]
}
fn read_number(&mut self) -> &'a str {
let start = self.position;
while let Some(ch) = self.peek() {
if ch.is_ascii_digit() || ch == '.' {
self.advance();
} else {
break;
}
}
&self.input[start..self.position]
}
fn read_string(&mut self) -> &'a str {
let start = self.position;
self.advance(); // 跳过开头的引号
while let Some(ch) = self.peek() {
if ch == '"' {
self.advance();
break;
}
self.advance();
}
&self.input[start..self.position]
}
fn make_token(&self, kind: TokenKind, text: &'a str) -> Token<'a> {
Token {
kind,
text,
line: self.line,
column: self.column - text.len(),
}
}
pub fn next_token(&mut self) -> Option<Token<'a>> {
self.skip_whitespace();
let ch = self.peek()?;
let start_col = self.column;
let kind = if ch.is_alphabetic() || ch == '_' {
let text = self.read_identifier();
let kind = match text {
"fn" | "let" | "mut" | "if" | "else" | "loop" | "while" | "for" | "in" | "return" => TokenKind::Keyword,
_ => TokenKind::Identifier,
};
return Some(Token { kind, text, line: self.line, column: start_col });
} else if ch.is_ascii_digit() {
let text = self.read_number();
return Some(Token { kind: TokenKind::Number, text, line: self.line, column: start_col });
} else if ch == '"' {
let text = self.read_string();
return Some(Token { kind: TokenKind::String, text, line: self.line, column: start_col });
} else if ch == '\n' {
self.advance();
return Some(Token { kind: TokenKind::Newline, text: "\n", line: self.line - 1, column: start_col });
} else if "+-*/=<>!&|".contains(ch) {
self.advance();
return Some(Token { kind: TokenKind::Operator, text: &self.input[self.position - 1..self.position], line: self.line, column: start_col });
} else {
self.advance();
return Some(Token { kind: TokenKind::Unknown, text: &self.input[self.position - 1..self.position], line: self.line, column: start_col });
};
}
pub fn tokenize(&mut self) -> Vec<Token<'a>> {
let mut tokens = Vec::new();
while let Some(token) = self.next_token() {
tokens.push(token);
}
tokens
}
}
fn main() {
let source = r#"
fn main() {
let x = 42;
let name = "hello";
if x > 10 {
return x;
}
}
"#;
let mut lexer = Lexer::new(source);
let tokens = lexer.tokenize();
println!("{:<15} {:<10} {:<5} {}", "KIND", "TEXT", "LINE", "COL");
println!("{}", "-".repeat(40));
for token in tokens {
println!("{:<15?} {:<10} {:<5} {}", token.kind, token.text, token.line, token.column);
}
}
这个例子展示了:
- 结构体持有引用时的生命周期标注
- 方法返回引用切片
- 迭代器与生命周期的配合
📝 本章小结
本章我们深入学习了 Rust 的生命周期系统:
- 生命周期确保引用不会比它指向的数据活得更长
- 生命周期标注使用
'a语法,描述引用之间的关系 - 省略规则在大多数情况下让编译器自动推断
- <code>'static</code> 表示程序整个运行期间有效的引用
- 高阶生命周期
for<'a>用于复杂场景
关键要点:
- 生命周期标注不会改变引用实际存活的时间
- 当编译器无法推断时,需要显式标注
- 理解三条省略规则可以简化代码
- 不要滥用
'static解决生命周期问题
在下一章,我们将学习智能指针——Rust 内存管理的另一层抽象。
动手实验:
- 编写一个函数
first_character(s: &str) -> Option<char>,思考为什么不需要显式生命周期标注。- 定义一个结构体
Pair<'a, 'b>,持有两个不同生命周期的字符串引用,并实现方法返回较长的那个。- 尝试编译以下代码,解释错误原因并修复:
``
rust fn get_ref<'a>() -> &'a String { &String::from("hello") }``