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 { ... }

这读作:参数 xy 至少活得和 'a 一样长,返回值也是如此。换句话说,返回值的生命周期不能超过 xy 中较短的那个。

3. 结构体中的生命周期

如果结构体内部包含引用,那么这个结构体本身就不能活得比它引用的数据长

1struct ImportantExcerpt<'a> {
2    part: &'a str,
3}

这意味着 ImportantExcerpt 的实例在销毁前,它引用的 part 必须一直有效。

4. 生命周期省略规则 (Elision Rules)

你可能会问:为什么 fn main() 里那么多引用没加 'a 也能编译? 因为 Rust 编译器总结了一些规则,自动帮我们补全了常见的生命周期模式。

  1. 每个引用参数都有自己的生命周期参数。
  2. 如果你只有一个输入引用参数,那么它被赋予所有输出生命周期。
  3. 如果方法有 &self,那么 self 的生命周期被赋予所有输出生命周期。

只有当这些规则不够用时,才需要手动标注。

5. ‘static 生命周期

这是个特例。'static 表示引用可以存活于整个程序运行期间。 所有字符串字面量 let s = "hello" 的类型其实是 &'static str

6. 小结

第十六篇笔记。我们翻过了 Rust 中最陡峭的一座山。

  • 生命周期是泛型的特例,用于保证引用的有效性。
  • Borrow Checker 负责在编译期检查这些约束。
  • 标注生命周期 'a 不会改变对象的实际存活时间,它只是帮编译器进行检查。

下一篇,我们将学习 自动化测试。Rust 对测试的支持是原生的,不像 Go 还需要 testing 包和特殊的函数命名约定,Rust 的测试写起来非常顺手。


练习题

  1. 尝试编写一个结构体,持有另一个结构体的引用,并为其实现 Display Trait。
  2. 思考:为什么 Rust 不允许在结构体中存储该结构体自身字段的引用(自引用结构体)?

思考题

Go 语言通过逃逸分析 (Escape Analysis) 决定变量在堆还是栈,从而管理内存。Rust 的生命周期检查和逃逸分析有什么异同?


本文代码示例

关注公众号:极客老墨

更多 AI 应用开发、工程实践和效率工具分享,欢迎扫码关注。

极客老墨微信公众号二维码

相关阅读