为什么 Rust 强制你指定生命周期?这又如何简化生活?
\n\n如果你正在学习 Rust,迟早会遇到这种神秘的语法:'a, 'b... 然后自然会有一个问题:“为什么我必须显式指定生命周期?其他语言不是也能处理吗!”
这个问题是 Stack Overflow 上最热门的问题之一(有数千次浏览)。这并不奇怪:Rust 中的 lifetimes 这个概念在你理解其本质之前,看起来确实很复杂。让我们通过生动的例子来理解,而不是枯燥的理论。
\n\n简短的回答:没有垃圾回收器的安全性
\n\nRust 保证编译时的内存安全。它需要精确知道引用的存活时间,以防止悬垂引用 (dangling references)。在 Java 或 Go 中,这由垃圾回收器负责。在 C++ 中,你自己负责内存管理(并且经常出错)。Rust 选择了第三条路:编译器静态地检查所有内容。
\n\n但为什么编译器不能自己推断出来?它可以——在 80% 的情况下。这被称为 lifetime elision(隐式推断规则)。然而,在复杂的情况下,它需要你的帮助。
\n\n规则 #1:编译器不是读心者
\n\n看看这个函数:
\n\nfn longest(x: &str, y: &str) -> &str {\n if x.len() > y.len() { x } else { y }\n}\n\n乍一看,一切都很合理。但 Rust 会报错:“missing lifetime specifier”。为什么?
\n\n想象一下,你传入了 x —— 一个存活 5 秒的字符串引用,以及 y —— 一个存活 1 秒的字符串引用。这个函数可能返回其中任何一个。返回的引用的生命周期会是多长?编译器不知道!
解决方案是显式声明:“返回的引用将与两个输入引用存活时间一样长”:
\n\nfn longest<'a>(x: &'a str, y: &'a str) -> &'a str {\n if x.len() > y.len() { x } else { y }\n}\n\n符号 'a 是一个生命周期注解。我们是在说:“这三个引用必须具有相同的最小生命周期”。现在编译器可以检查调用代码是否过早地销毁了数据。
规则 #2:不同的引用——不同的生命周期
\n\n初学者的一个常见错误是认为所有 'a 都必须相同。实际上,你可以指定不同的生命周期:
fn example<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {\n x // 没问题,返回生命周期为 'a 的引用\n // y // 错误!y 的生命周期是 'b,但我们承诺的是 'a\n}\n\n这里我们明确说明:x 和 y 可以有不同生命周期。但我们只能返回其生命周期与返回类型中承诺的生命周期相匹配的引用。
规则 #3:结构体也可以存储引用
\n\n当你创建一个包含引用的结构体时,Rust 中的生命周期必须显式指定:
\n\nstruct Book<'a> {\n title: &'a str,\n author: &'a str,\n}\n\nfn main() {\n let title = String::from("Rust Book");\n let book;\n {\n let author = String::from("John Doe");\n book = Book { title: &title, author: &author };\n } // author 在此处被销毁!\n println!("{}", book.title); // 错误!book.author 悬垂了\n}\n\n编译器不会允许这段代码编译,因为 book.author 引用了一个已被删除的字符串。Rust 中的显式 lifetimes 允许在编译时捕获此类错误,而不是在运行时。
规则 #4:Lifetime Elision —— 何时可以省略不写
\n\nRust 足够聪明,可以在简单的情况下自行推断生命周期。以下情况可以省略不写:
\n\n- \n
- 一个输入引用 —— 输出的生命周期等于输入的生命周期 \n