Rust 生命周期
很多人学 Rust,第一次真正“撞墙”就是在生命周期这里。这一页会讲得轻松一些:目标只是让你认出生命周期是什么、编译器为什么会向你要它,而不是一口气掌握所有相关规则。
生命周期要解决的问题
Section titled “生命周期要解决的问题”在借用那一页你已经知道,引用(&)可以让你使用一个值而不拥有它。但引用只有在它指向的值还存在时才有效。Rust 必须确保你永远不会拿到一个指向“已经被清理掉的东西”的引用。这类 bug 叫悬垂引用(dangling reference),在 C 这样的语言里是程序崩溃的经典原因。
大多数时候,Rust 通过观察代码里的作用域就能自己搞清楚,你根本不用操心(这正是你在作用域那一页看到的)。但有时候,尤其是在函数里,编译器没法自己推断出来,就会请你把它写清楚。这就是生命周期注解(lifetime annotation)。
一个需要帮 Rust 一把的函数
Section titled “一个需要帮 Rust 一把的函数”下面这个函数返回两个字符串 slice 中较长的那个:
fn longest(a: &str, b: &str) -> &str { if a.len() > b.len() { a } else { b }}它编译不过,错误信息是这样的:
error[E0106]: missing lifetime specifier意思是“缺少生命周期说明符”。问题在于:返回的引用可能借自 a,也可能借自 b,Rust 光看函数本身分辨不出是哪一个,所以它没法保证你拿到的引用在你使用时依然有效。
加上生命周期注解
Section titled “加上生命周期注解”修复的方法是给生命周期起个名字(按照惯例,用一个小写字母,比如 'a),然后告诉 Rust:返回值的生命周期和两个参数绑在一起:
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() > b.len() { a } else { b }}
fn main() { let s1 = String::from("hello"); let s2 = String::from("hi");
let result = longest(&s1, &s2); println!("较长的是:{result}");}输出:
较长的是:hello把 'a 看成一个标签,而不是一个值:它并不规定实际的时长。它只是告诉 Rust:“这些引用至少要在同一段代码里都保持有效。”然后编译器会检查每一个调用的地方,确认这件事真的成立。
为什么这很重要
Section titled “为什么这很重要”没有这个注解,编译器真的分辨不出 longest 返回的引用会不会比它来源的某个字符串活得更久。有了生命周期注解,编译器就能在编译期证明(运行时零开销):你拿到的引用在使用时一定有效。
持有引用的结构体
Section titled “持有引用的结构体”你接下来会在结构体里遇到生命周期。到目前为止,结构体的每个字段都拥有自己的数据,比如 String。字段也可以是引用,但这样一来,结构体只有在它指向的数据还存在时才有效。Rust 要求你用生命周期把这一点写出来:
struct Highlight<'a> { text: &'a str, // 借用别处的某个 String}
fn main() { let book = String::from("Call me Ishmael. Some years ago..."); let first = Highlight { text: &book[..15] };
println!("摘录:{}", first.text); println!("整本书一共 {} 个字节", book.len());}输出:
摘录:Call me Ishmael整本书一共 34 个字节把 Highlight<'a> 读作“一个在生命周期 'a 内借用了某样东西的 Highlight”。&book[..15] 是一个 slice:它借用 book 的前 15 个字节,而不复制它们。书里的文字保留英文,因为 slice 是按字节切的,一个汉字占 3 个字节,换成中文就切不对了。因为 first 只是借用,所以之后 book 还能继续用。
有了生命周期,Rust 就能抓住下面这种错误:String 已经被丢弃了,结构体却还指向它:
struct Highlight<'a> { text: &'a str,}
fn main() { let highlight; { let book = String::from("Call me Ishmael. Some years ago..."); highlight = Highlight { text: &book[..15] }; } // book 在这里被丢弃……
println!("{}", highlight.text); // ……但 highlight 还指向它}error[E0597]: `book` does not live long enough --> src/main.rs:9:40 | 8 | let book = String::from("Call me Ishmael. Some years ago..."); | ---- binding `book` declared here 9 | highlight = Highlight { text: &book[..15] }; | ^^^^ borrowed value does not live long enough10 | } // book 在这里被丢弃…… | - `book` dropped here while still borrowed11 |12 | println!("{}", highlight.text); // ……但 highlight 还指向它 | -------------- borrow later used here这条错误的意思是“book 活得不够久”:book 在 } 处被丢弃时还处于被借用的状态,而这个借用在后面又被用到了。
一个实用的小建议:如果结构体里的生命周期让你很头疼,就改成存一个拥有所有权的 String,而不是 &str。这样会复制一份文字,但结构体不再依赖任何别的东西,生命周期也就消失了。很多有经验的 Rust 程序员一开始都这么写,等到有理由时才换成引用。
你实际写 'a 的次数,会比这一页给你的印象少得多。大量 Rust 代码根本不需要显式的生命周期,因为在大多数日常场景下,编译器都能自己推断出来。你主要会在函数返回引用、而编译器又推断不出引用从哪里来时遇到它。到那时只要记住:这不是 bug,而是编译器在请你确认一件它自己证明不了的事。
1. 较短的字符串
Section titled “1. 较短的字符串”写一个函数 shortest,接收两个字符串 slice,返回较短的那个。如果两个一样长,就返回第一个。用 "elephant" 和 "cat" 调用它。
提示:它和上面的 longest 形状一样。函数名后面需要 <'a>,两个参数和返回类型上都要写 'a。
查看答案
fn shortest<'a>(a: &'a str, b: &'a str) -> &'a str { if a.len() <= b.len() { a } else { b }}
fn main() { let big = String::from("elephant"); let small = String::from("cat");
println!("较短的是:{}", shortest(&big, &small)); println!("一样长时取第一个:{}", shortest("dog", "cow"));}输出:
较短的是:cat一样长时取第一个:dog返回的 slice 可能来自 a,也可能来自 b,所以 Rust 需要 'a 才能知道:结果只在两个输入都有效时才有效。用 <= 而不是 <,一样长时就会选 a。
2. 借用数据的结构体
Section titled “2. 借用数据的结构体”定义一个结构体 Quote,它有两个 &str 字段:text 和 author。创建一个实例:text 借用 String "Fearless concurrency is a goal of Rust" 的前 20 个字节,author 是字面量 "The Rust Book"。把它打印成 Fearless concurrency (The Rust Book)。
提示:和上面的 Highlight 一样,这个结构体需要生命周期:struct Quote<'a>,两个字段都用 &'a str。&speech[..20] 就是前 20 个字节的 slice。
查看答案
struct Quote<'a> { text: &'a str, author: &'a str,}
fn main() { let speech = String::from("Fearless concurrency is a goal of Rust"); let quote = Quote { text: &speech[..20], author: "The Rust Book", };
println!("{} ({})", quote.text, quote.author);}输出:
Fearless concurrency (The Rust Book)quote 借用了 speech,所以它不能比 speech 活得更久。字面量 "The Rust Book" 直接写在程序里,整个运行期间都存在,所以它轻松满足 'a 的要求。试着把 speech 放进一个内部的 { } 块里创建,然后在块结束后打印 quote,就像上面那个出错的例子一样,看看编译器怎么拦住你。
- 生命周期告诉编译器引用之间是什么关系,好让它排除悬垂引用。
- 只有当编译器自己推断不出来时才需要手写,最常见的是返回引用的函数。
'a并不规定时长,它把不同引用的生命周期联系起来,让编译器去检查。- 持有引用的结构体也需要生命周期(
struct Highlight<'a>),而且不能比它借用的数据活得更久。改存拥有所有权的String就能避开这个问题。 - 这完全是编译期的检查,不会增加任何运行时开销。
小测验
1. 生命周期解决的是什么问题?
2. 为什么 fn longest(a: &str, b: &str) -> &str 不加生命周期注解就编译不过?
3. 加上生命周期注解会带来运行时开销吗?
4. 按照这一页的说法,什么时候最常需要显式写出生命周期?
得分: 0 / 4
- 生命周期确保引用有效 — Rust 程序设计语言:官方的完整讲解,包括结构体中的生命周期、省略规则和
'static。 - 函数 — 通过例子学 Rust:带生命周期注解的函数的简短示例,以及编译器会拒绝哪些写法。
- 结构体 — 通过例子学 Rust:更多持有引用的结构体和枚举。
- Lifetime elision — The Rust Reference(英文):编译器自动补全生命周期时用的精确规则,让你知道什么时候可以省略不写。