生命周期:证明引用的有效性
你将学到什么: 为什么生命周期会存在(编译器必须拿到安全性证明)、生命周期标注语法 (
'a)、省略规则(为什么你经常不需要手写它们),以及结构体中的生命周期。难度: 高级
C# 开发者通常不会思考“引用的生命周期”这个问题,因为垃圾回收器 (GC) 会负责对象的可达性。而在 Rust 中,编译器必须拿到证明,确认每个引用在被使用期间都始终有效。生命周期就是这份证明。
为什么生命周期会存在
考虑一个接收两个引用并返回其中之一的函数:
#![allow(unused)]
fn main() {
fn longest(a: &str, b: &str) -> &str {
if a.len() > b.len() { a } else { b }
}
}
编译器会拒绝这段代码,因为它不知道返回的引用到底是借用了 a 还是 b。如果 b 离开了作用域,而调用者仍在尝试使用结果,就会产生悬垂指针。
生命周期标注语法
你使用 'a 语法来告诉编译器:“返回值的生命周期至少与输入变量们一样长。”
#![allow(unused)]
fn main() {
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b }
}
}
重要提示: 生命周期标注不会改变值的存活时间。它们仅仅是在描述不同引用之间的存活关系,以便编译器进行校验。
生命周期省略规则
在大多数情况下,你并不需要手写 'a。编译器会自动应用三条规则来进行自动推断:
- 每一个输入引用都会获得其独立的生命周期。
- 如果只有一个输入引用,那么它对应的生命周期会被赋给所有的输出。
- 如果输入中包含
&self或&mut self,那么该生命周期会被赋给所有的输出。
#![allow(unused)]
fn main() {
// 编译器会自动将这段代码:
fn first_word(s: &str) -> &str { ... }
// 推断为:
fn first_word<'a>(s: &'a str) -> &'a str { ... }
}
结构体中的生命周期
如果一个结构体中包含引用,那么它必须带有生命周期标注。这确保了结构体本身的存活时间不能超过它所引用的数据。
#![allow(unused)]
fn main() {
struct Excerpt<'a> {
text: &'a str,
}
let novel = String::from("请叫我以实玛利。");
let first_sentence = Excerpt { text: &novel };
// 如果 'novel' 被释放了,'first_sentence' 也不能再继续存在。
}
'static 生命周期
'static 被称为静态生命周期,意味着引用能够在程序的整个运行期间一直存活。
- 字符串字面量:
"Hello"永远都是&'static str,因为它被直接编码进了程序的二进制文件里。 - 全局常量:通常也是
'static。
C# 开发者总结
| 概念 | C# 对应物 | Rust 现实 |
|---|---|---|
| 对象寿命 | 由 GC 动态管理 | 由作用域/所有权静态定义 |
| 引用跟踪 | 运行期 (是否可达?) | 编译期 (是否 Life’a?) |
| 局部变量引用 | 不允许或通过 Box 安全化 | 被借用检查器严格限制 |
| 包含引用的结构体 | 对 Class 而言没法直接实现 | 需要显式标注 <'a> |
练习:添加生命周期标注
挑战: 为一个涉及多个引用的结构体和函数补全生命周期标注。
#![allow(unused)]
fn main() {
struct Profile<'a> {
username: &'a str,
}
fn get_username<'a>(p: &'a Profile) -> &'a str {
p.username
}
}
关键理解: 虽然省略规则能处理很多简单场景,但在构建为了追求极致性能而频繁借用数据的复杂数据结构时,深入理解 'a 是必不可少的。