Rust 学习笔记 16:生命周期 (Lifetimes)
“To live is to suffer, to survive is to find some meaning in the suffering… of the borrow checker.”
生命周期 (Lifetimes) 是 Rust 中最令人困惑的概念,没有之一。 在 Go 或 Java 中,GC 会自动管理对象的死活。你不需要关心引用活多久,反正 GC 兜底。 在 C++ 中,你需要手动管理,稍有不慎就是 Use-After-Free。
Rust 选择了第三条路:编译期验证引用有效性。这就是生命周期检查器 (Borrow Checker) 的工作。
1. 为什么需要生命周期?
主要为了避免悬垂引用 (Dangling References)。
1{
2 let r;
3 {
4 let x = 5;
5 r = &x;
6 } // x 在这里销毁了
7 println!("r: {}", r); // r 指向了非法内存
8}
这段代码无法通过编译,因为 r 的生命周期比 x 长。
2. 泛型生命周期注解
大多数时候,生命周期是隐式的。但在某些情况下,编译器推断不出来,需要你手动帮它一把。
经典例子:longest 函数。
1fn longest(x: &str, y: &str) -> &str {
2 if x.len() > y.len() { x } else { y }
3}
如果不加注解,编译器会报错:我怎么知道返回值引用的是 x 还是 y?如果 x 活得长,y 活得短,返回值能活多久?
加上注解 'a:
1fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { ... }
这读作:参数 x 和 y 至少活得和 'a 一样长,返回值也是如此。换句话说,返回值的生命周期不能超过 x 和 y 中较短的那个。
3. 结构体中的生命周期
如果结构体内部包含引用,那么这个结构体本身就不能活得比它引用的数据长。
1struct ImportantExcerpt<'a> {
2 part: &'a str,
3}
这意味着 ImportantExcerpt 的实例在销毁前,它引用的 part 必须一直有效。
4. 生命周期省略规则 (Elision Rules)
你可能会问:为什么 fn main() 里那么多引用没加 'a 也能编译?
因为 Rust 编译器总结了一些规则,自动帮我们补全了常见的生命周期模式。
- 每个引用参数都有自己的生命周期参数。
- 如果你只有一个输入引用参数,那么它被赋予所有输出生命周期。
- 如果方法有
&self,那么self的生命周期被赋予所有输出生命周期。
只有当这些规则不够用时,才需要手动标注。
5. ‘static 生命周期
这是个特例。'static 表示引用可以存活于整个程序运行期间。
所有字符串字面量 let s = "hello" 的类型其实是 &'static str。
6. 小结
第十六篇笔记。我们翻过了 Rust 中最陡峭的一座山。
- 生命周期是泛型的特例,用于保证引用的有效性。
- Borrow Checker 负责在编译期检查这些约束。
- 标注生命周期
'a不会改变对象的实际存活时间,它只是帮编译器进行检查。
下一篇,我们将学习 自动化测试。Rust 对测试的支持是原生的,不像 Go 还需要 testing 包和特殊的函数命名约定,Rust 的测试写起来非常顺手。
练习题:
- 尝试编写一个结构体,持有另一个结构体的引用,并为其实现
DisplayTrait。 - 思考:为什么 Rust 不允许在结构体中存储该结构体自身字段的引用(自引用结构体)?
思考题:
Go 语言通过逃逸分析 (Escape Analysis) 决定变量在堆还是栈,从而管理内存。Rust 的生命周期检查和逃逸分析有什么异同?
本文代码示例:
关注公众号:极客老墨
更多 AI 应用开发、工程实践和效率工具分享,欢迎扫码关注。
