第 9 章 生命周期
第 9 章 生命周期(深化)
“在 Rust 的世界里,每一个引用都有自己的保质期,过期的引用就像超市里过期的酸奶——编译器会让你尝到什么叫’酸爽’。”
想象一下,你借了一本书给别人,结果那个人比书还早消失在这个世界上——这在现实生活里可能是个感人的故事,但在 Rust 编译器眼里,这叫"悬空引用"(Dangling Reference),是要被严惩的重罪!
生命周期(Lifetimes)就是 Rust 编译器用来追踪"这个引用到底能活多久"的超级管家。它不像 JavaScript 那样等到运行时才发现引用已经飞升(然后给你一个 null),也不像 C 那样让程序带着悬空指针裸奔到崩溃。Rust 在编译期就把这事儿安排得明明白白——没有编译通过,你就别想跑起来。
这一章,我们要把生命周期这个概念翻来覆去、揉碎掰开、嚼烂了再咽下去。准备好了吗?Let’s go!
9.1 生命周期详解
9.1.1 生命周期标注规则
9.1.1.1 单生命周期参数:‘a
好,我们先从最简单的开始。
在 Rust 里,生命周期参数长得就像一个小尾巴——以单引号开头,后面跟一个名字。就像给你的引用贴上一个"此引用有效期至 X"的标签。
1
2
3
4
5
6
7
8
| // 这是一个带生命周期标注的函数签名
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
|
这里的 'a 就是生命周期参数,它的意思是:返回的引用的生命周期,不会超过输入的两个引用的生命周期中较短的那个。
等等,你可能在想:我能不能不给生命周期标注,让编译器自己推断?答案是——能,但不是在所有情况下都能。Rust 有一条"生命周期省略规则"(Elision),可以在某些情况下自动推断。但如果编译器实在推断不出来,它就会友情提示你:“嘿,兄弟,这个引用我没法自动推断它的寿命,麻烦你告诉我它能活多久?”
longest 函数的例子就属于编译器无法推断的情况,所以我们必须手动标注 'a。这个 'a 可以理解为一个"生命周期占位符",它代表了 x 和 y 这两个输入引用的生命周期中较短的那个。
9.1.1.2 多生命周期参数:‘a / ‘b / ‘c
一个生命周期参数不够用?那就再来几个!
1
2
3
4
5
6
7
8
9
| fn mix_and_match<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
println!("混搭一下:{} 和 {}", x, y); // 混搭一下:hello 和 world
x // 返回 x,所以返回类型只需要 'a
}
fn main() {
let result = mix_and_match("hello", "world");
println!("结果是:{}", result); // 结果是:hello
}
|
当你有多个引用,并且它们之间没有直接关系的时候,就需要多个生命周期参数。
1
2
3
4
5
6
7
8
| // 三个独立的生命周期,各玩各的
fn three_some<'a, 'b, 'c>(s1: &'a str, s2: &'b str, s3: &'c str) {
println!("三个独立生命周期:{} / {} / {}", s1, s2, s3);
}
fn main() {
three_some("a", "b", "c"); // 三个独立生命周期:a / b / c
}
|
不过要注意,不是用得越多越好。如果你写了 'a, 'b, 'c 但实际上它们都是同一个生命周期,那纯属给自己找麻烦。生命周期参数的选择原则是:能共用就共用,不能共用再分开。
9.1.1.3 生命周期约束:T: ‘a(T 不含任何生命周期短于 ‘a 的引用)
生命周期约束听起来很拗口,但其实它是在说:“T 这个类型里所有的引用,它们活得都得比 ‘a 久”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // T: 'a 意味着类型 T 中不能有任何生命周期短于 'a 的引用
// ⭐ 注意:'a 必须在泛型参数表里先声明出来,否则编译器会报
// error[E0261]: use of undeclared lifetime name `'a`
fn requires_outlives<'a, T>(value: &'a T) -> &'a T
where
T: 'a,
{
value
}
fn main() {
let n = 42;
println!("{}", requires_outlives(&n)); // 42
// 下面这种才是 T: 'a 真正想拦下来的情况:
// 局部字符串活不过 'a,编译器不会放行
// let short = String::from("短命");
// let r = requires_outlives(&short.as_str());
}
|
💡 顺带说明:value: &'a T 这个签名本身就已经隐含了 T: 'a
(一个 &'a T 要想成立,T 就必须活满 'a),所以这里的 where T: 'a
属于"重复标注"。真正有价值的场景是只有 T、没有 &'a T 的时候:
1
2
3
4
5
6
7
| // 这里没有任何 &'a T,T: 'a 约束才是必要信息
fn store_later<'a, T>(value: T) -> Box<dyn Fn() -> T + 'a>
where
T: 'a + Clone,
{
Box::new(move || value.clone())
}
|
这个约束在泛型编程中超级有用。比如,你想写一个函数,它接受一个结构体,这个结构体里可能有很多引用,但你希望这些引用至少跟你的函数一样"长寿"。
小剧场:如果你写过 Java 或者 Go,这种约束可能会让你想起泛型约束。但是 Rust 的生命周期约束更严格,因为它直接跟内存安全挂钩。你可以把 T: 'a 理解为"我要的是那种能活到 ‘a 的 T",就像招聘要求里写的"需要能工作到 35 岁"——不过 Rust 没有年龄歧视,它只关心引用。
9.1.2 多生命周期的场景
9.1.2.1 函数有多于一个生命周期参数
在现实编程中,我们经常会遇到一个函数需要处理多个没有关联的引用。比如:
1
2
3
4
5
6
7
8
9
| // 返回第一个参数,不关心第二个参数的生命周期
fn first_one<'a, 'b>(s1: &'a str, _s2: &'b str) -> &'a str {
s1
}
fn main() {
let result = first_one("hello", "world");
println!("第一个是:{}", result); // 第一个是:hello
}
|
在这个例子里,'a 和 'b 完全是两个独立的生命周期。编译器会确保 s1 的生命周期至少覆盖返回值,而 s2 爱活多久活多久,反正我们不用它。
9.1.2.2 生命周期参数之间的关系
有时候,多个生命周期参数之间需要建立联系。比如:
1
2
3
4
5
6
7
8
9
10
11
| // 返回值的生命周期跟第二个参数 'b 绑定(因为我们返回的是 y)
fn combine<'a, 'b>(_x: &'a str, y: &'b str) -> &'b str {
// 注意:这里我们实际上返回的是 y,不是 x
// 所以返回类型是 &'b,不是 &'a
y
}
fn main() {
let result = combine("first", "second");
println!("结果是:{}", result); // 结果是:second
}
|
如果你的函数有多个返回值引用,而这些返回值分别来自不同的输入参数,那你可能需要仔细考虑它们之间的关系。
warning:别忘了,如果函数返回的是引用,那这个引用必须来自输入参数之一。如果你写的是 return &some_local_variable,那你就是在制造悬空引用——这可是 Rust 编译器最讨厌的事情,编译不通过那种!
9.1.3 生命周期省略规则(Elision)
9.1.3.1 输入生命周期省略规则(参数中的引用自动获得生命周期)
好的,铺垫了这么久,终于到了 Rust 编译器"做好事"的部分了。
好消息:Rust 编译器其实挺智能的,它会自动推断一些简单的生命周期,而不需要你手动标注。这就是"生命周期省略规则"(Elision)。
坏消息:它不是万能的,有些情况下它推断不出来,就得靠你手动标注。
更坏的消息:如果你手动标注错了,编译器会毫不客气地报错——报错信息有时候长得能绕地球三圈。
好了,来看省略规则吧。规范里一共三条,顺序很重要——先看输入,再看输出:
规则 1(输入侧):每个被省略的输入生命周期,各自变成一个独立的生命周期参数。
1
2
3
4
5
6
7
8
9
| // 你写的:
fn two_inputs(x: &str, y: &str) { }
// 编译器眼里等价于('a 和 'b 是两个不同的生命周期):
fn two_inputs<'a, 'b>(x: &'a str, y: &'b str) { }
// ⭐ 这一条是"分成两个",不是"共用同一个",
// 所以下面这种签名照样编译不过(返回类型不知道该用哪个):
// fn pick(x: &str, y: &str) -> &str { x } // error[E0106]
|
规则 2(输出侧):如果输入侧只有一个生命周期(省略后的也算),那么所有被省略的输出生命周期都等于它。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 只有一个输入引用 → 输出自动取它的生命周期
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
// 等价于 fn first_word<'a>(s: &'a str) -> &'a str
// 显式标注(没有省略)时,这条规则同样适用:
// 只要"有效的输入生命周期"只有一个,输出省略后也取它
fn first_word2<'a>(s: &'a str) -> &str {
first_word(s)
}
// ⚠️ 不过较新的 rustc 会对这种"输入写了 'a、输出又省略"的写法给出
// mismatched_lifetime_syntaxes 警告(说的是同一个生命周期,却用了两种写法)。
// 能编译,但为了可读性,实际项目里建议写全:-> &'a str
|
规则 3(输出侧补充):如果有多个输入生命周期,但其中一个是 &self 或 &mut self,那么输出的生命周期取 self 的。
1
2
3
4
5
6
7
8
| struct X;
impl X {
// 编译器自动推断为 fn bar<'a>(&'a self) -> &'a str
fn bar(&self) -> &str {
"hello"
}
}
|
⚠️ 注意规则 3 的措辞:只有 self 才能"独占"输出的生命周期。
如果方法没有 self,哪怕参数里只有一个引用,规则 2 也能生效;
但如果没有 self 又有多个输入引用,输出就必须显式标注了(见下一小节)。
9.1.3.2 输出生命周期省略规则(返回值引用从参数推断)
一句话总结上一节的规则 2/3:输出生命周期只有在"能唯一确定来源"时才会被省略。
1
2
3
4
5
6
7
8
9
| // 等价于 fn first_char<'a>(s: &'a str) -> &'a str
fn first_char(s: &str) -> &str {
&s[..1]
}
fn main() {
let result = first_char("hello");
println!("第一个字符是:{}", result); // 第一个字符是:h
}
|
⚠️ 上面这个 &s[..1] 只是"能跑",并不是"写法正确",它有两个隐藏的坑:
1
2
3
4
5
| // 坑 1:空字符串会直接 panic(index out of bounds)
// first_char("");
// 坑 2:多字节字符不是按"字符"切,而是按"字节"切,会 panic
// first_char("你好"); // byte index 1 is not a char boundary
|
稳妥的写法是按字符取:
1
2
3
4
5
6
7
8
9
10
11
12
| fn first_char(s: &str) -> &str {
match s.char_indices().nth(1) {
Some((idx, _)) => &s[..idx], // 第一个字符的结束位置
None => s, // 说明整个字符串就是一个字符
}
}
fn main() {
println!("{}", first_char("hello")); // h
println!("{}", first_char("你好")); // 你
println!("{}", first_char("")); // (空)
}
|
9.1.3.3 无法省略时的显式标注(编译器 E0106 / E0107)
当省略规则无法推断出生命周期时,编译器会给你一个 E0106 或 E0107 错误。这两个错误就像编译器在说:“兄弟,我真的猜不出来,你自己告诉我吧!”
1
2
3
4
5
6
7
8
9
10
11
| // 这个函数无法省略生命周期标注
// 编译器报错:E0106
fn ambiguous<'a, 'b>(x: &'a str, y: &'b str) -> &str {
// 编译器不知道返回的是 x 还是 y
// 所以无法确定返回引用的生命周期
if x.len() > y.len() {
x
} else {
y
}
}
|
正确的写法:
1
2
3
4
5
6
7
8
9
| fn ambiguous<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
// 明确告诉编译器,我们返回的是 x,所以生命周期是 'a
if x.len() > y.len() {
x
} else {
y // 等等,这里返回的是 y,它的生命周期是 'b,不是 'a!
// 编译器会报错!因为你承诺了返回 'a,但实际可能返回 'b
}
}
|
再正确一点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| fn longest_with_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: std::fmt::Display,
{
println!("公告:{}", ann); // 公告:这是一个比较
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let result = longest_with_announcement("short", "very_long", "这是一个比较");
println!("更长的那个是:{}", result); // 更长的那个是:very_long
}
|
9.1.4 生命周期子类型
9.1.4.1 ‘a: ‘b(‘a outlives ‘b,‘a 不比 ‘b 短)
终于到了"子类型"这个听起来很高级的概念了。
在 Rust 的生命周期体系里,'a: 'b 意思是 'a 至少要活得跟 'b 一样久,或者更久。你可以理解为 'a 是 'b 的"老子"——'a outlives 'b。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 这里 'long 至少要活得跟 'short 一样久
fn longest<'long: 'short, 'short>(
x: &'long str,
y: &'short str,
) -> &'short str {
if x.len() > y.len() {
x // ✅ 这行是完全合法的,不会报错!
} else {
y
}
}
fn main() {
println!("{}", longest("aa", "b")); // aa
}
|
⭐ 这里要特别纠正一个常见误解:很多人以为"返回 x 会报错,因为 x 是 &'long str 而返回类型是 &'short str"。
实际上不会报错——因为 'long: 'short 保证了 'long 比 'short 长,
所以 &'long str 可以安全地当成 &'short str 用,编译器会自动做这层"缩短"转换。
这就是所谓的生命周期子类型:生命周期更长 ⇒ 类型更强(是子类型)。
方向千万别记反——是"长命引用可以当短命引用用",而不是反过来。
反过来才会报错:
1
2
3
4
5
6
7
8
9
10
| // 把两个参数的约束关系写反,编译器立刻不答应
fn bad_return<'long, 'short: 'long>(
x: &'long str,
_y: &'short str,
) -> &'short str {
x // ❌ 编译不通过
// 因为此时并没有 "x 比返回值活得久" 的保证
}
fn main() {}
|
编译这份代码会得到:
error: lifetime may not live long enough
--> src/main.rs:5:5
|
1 | fn bad_return<'long, 'short: 'long>(
| ----- ------ lifetime `'short` defined here
| |
| lifetime `'long` defined here
...
5 | x
| ^ function was supposed to return data with lifetime `'short` but it is returning data with lifetime `'long`
|
= help: consider adding the following bound: `'long: 'short`
error: aborting due to 1 previous error
💡 编译器给出的建议正好说明了问题:它让我们把约束改成 'long: 'short(也就是上面那个能通过的版本)。
9.1.4.2 生命周期子类型验证
生命周期子类型最常见的应用场景是在结构体里:
1
2
3
4
5
6
7
8
| // 'a: 'b 意味着 'a 必须活得比 'b 久(或一样久)
struct Wrapper<'a, 'b>
where
'a: 'b,
{
data: &'a str, // 这个引用活得久
maybe_shorter: &'b str, // 这个引用可以活得短
}
|
生活类比:把生命周期想成你的银行账户。'a 是你的储蓄账户,'b 是你的信用卡账户。如果 'a: 'b,就意味着你的储蓄账户余额永远不少于你的信用卡欠款——这是一个好习惯!
9.1.5 生命周期与引用返回
9.1.5.1 返回引用的生命周期必须来自参数
这是 Rust 生命周期规则中最重要的一条:函数返回的引用,其生命周期必须来自输入参数。
1
2
3
4
5
6
7
8
9
10
| // ✓ 正确:返回的生命周期来自输入参数
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
// ✗ 错误:制造悬空引用
fn dangling() -> &str {
let s = String::from("hello");
&s // 错误!s 是局部变量,函数结束就被销毁了
}
|
编译上述代码,你会得到:
error[E0515]: cannot return reference to local variable `s`
编译器用一种优雅的方式告诉你:“你返回了一个局部变量的引用,这个变量在函数结束时就会去领盒饭(drop),你不能这样做!”
9.1.5.2 输入生命周期与输出生命周期的关系
在实际编码中,我们经常需要决定返回引用的生命周期跟哪个输入参数绑定。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // 返回第一个参数的生命周期
fn first<'a>(x: &'a str, _y: &str) -> &'a str {
x
}
// 返回两个参数中较短的生命周期(因为我们不确定返回哪个)
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("xyz");
result = longest(s1.as_str(), s2.as_str());
println!("最长的是:{}", result); // 最长的是:long string
}
// ⚠️ 这里如果把 result 拿到 s2 的作用域之外使用,会编译失败!
// println!("{}", result); // ❌ error[E0597]: `s2` does not live long enough
}
|
⭐ 这段代码是理解生命周期的"分水岭",值得停下来想清楚:
虽然运行时 longest 返回的确实是 s1 的引用(“long string” 更长),
但类型系统不看运行时。函数签名承诺的是"返回值活得和两个参数中较短的那个一样久",
所以 result 的类型是 &'a str,而这里的 'a 被推断为 s2 的生命周期。
s2 一离开作用域,result 就不能再用了——哪怕它实际指向的是 s1。
真实的编译器报错:
error[E0597]: `s2` does not live long enough
--> src/main.rs:7:39
|
6 | let s2 = String::from("xyz");
| -- binding `s2` declared here
7 | result = longest(s1.as_str(), s2.as_str());
| ^^ borrowed value does not live long enough
8 | }
| - `s2` dropped here while still borrowed
9 | println!("{}", result);
| ------ borrow later used here
如果只想返回 x,就把签名写得精确一点,这样返回值就只跟 s1 绑定:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 返回值只来自 x,y 的生命周期与返回值无关
fn first<'a>(x: &'a str, _y: &str) -> &'a str {
x
}
fn main() {
let s1 = String::from("long string");
let result;
{
let s2 = String::from("xyz");
result = first(s1.as_str(), s2.as_str());
} // s2 在这里被 drop,但已经没关系了
println!("{}", result); // ✅ 编译通过:long string
}
|
💡 经验法则:函数签名里少写一个生命周期参数,往往就能让调用方"更自由"。
在保证安全的前提下,让返回值的生命周期尽量只绑定它真正来自的那个参数,
这样既表达了真实意图,也不会给调用方凭空加上不必要的约束。
9.1.5.3 多个参数的生命周期推导
当函数有多个引用参数时,Rust 编译器会根据返回值的来源自动建立关系。
1
2
3
4
5
6
7
8
9
10
11
| // 编译器自动推断:
// - 返回值来自 x,所以返回生命周期 = x 的生命周期
// - y 跟返回生命周期无关
fn get_x<'a>(x: &'a str, y: &str) -> &'a str {
x // 明确返回 x
}
fn main() {
let result = get_x("hello", "world");
println!("{}", result); // hello
}
|
9.2 生命周期与结构体
9.2.1 结构体中引用的生命周期
9.2.1.1 struct &‘a str(引用字段必须标注生命周期)
结构体里如果有引用字段,那这个结构体就必须标注生命周期。这是因为结构体的寿命取决于它内部引用字段的寿命——结构体不能比它内部的任何一个引用活得更久。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // 经典例子:ImportantExcerpt 结构体
struct ImportantExcerpt<'a> {
part: &'a str, // 必须标注生命周期 'a
}
fn main() {
let novel = String::from("_call me Ishmael. Years ago...");
let first_sentence = novel.split('.').next().unwrap();
let excerpt = ImportantExcerpt {
part: first_sentence,
};
println!("摘录:{}", excerpt.part); // 摘录:_call me Ishmael
}
|
敲黑板:如果你的结构体里有引用字段,必须标注生命周期。这是 Rust 的强制要求,不标?编译器会用 E0106 错误码热情地招待你。
9.2.1.2 结构体实例化时必须提供生命周期
当你创建一个包含引用字段的结构体时,你必须确保提供的引用是"活得够久"的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| struct Holder<'a> {
data: &'a str,
}
fn main() {
// 正确:提供活得够久的引用
let static_string = "我活得很久很久";
let holder1 = Holder { data: static_string };
println!("holder1: {}", holder1.data); // holder1: 我活得很久很久
// 错误示范:
let local_string = String::from("我马上就要被销毁了");
// let holder2 = Holder { data: &local_string }; // 编译错误!
// local_string 是局部变量,函数结束就 drop 了
// 但 Holder 的生命周期不知道有多长,编译器不让你冒险
// 正确示范:在同一个作用域内使用
{
let short_lived = String::from("我活不长");
let holder2 = Holder { data: &short_lived };
println!("holder2: {}", holder2.data); // holder2: 我活不长
} // short_lived 和 holder2 在这里一起 drop
println!("holder1 还在:{}", holder1.data); // holder1 还在:我活得很久很久
}
|
9.2.2 生命周期省略在结构体中的规则
9.2.2.1 结构体方法的省略规则
结构体的方法也有生命周期省略规则,跟函数类似。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| struct Excerpt<'a> {
part: &'a str,
}
impl<'a> Excerpt<'a> {
// 这个方法只使用了 self 的引用
// 编译器自动推断返回生命周期 = self 的生命周期
fn announce_and_return(&self, announcement: &str) -> &str {
println!("公告:{}", announcement); // 公告:即将返回
self.part
}
}
fn main() {
let text = String::from("call me Ishmael...");
let excerpt = Excerpt { part: &text };
let result = excerpt.announce_and_return("即将返回");
println!("返回的内容是:{}", result); // 返回的内容是:call me Ishmael...
}
|
9.2.3 带生命周期的方法
9.2.3.1 impl<‘a> Struct<‘a>
当你在结构体上 impl 方法时,如果结构体有生命周期参数,你需要在 impl 块中也声明这个生命周期参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| struct Parser<'a> {
input: &'a str,
position: usize,
}
impl<'a> Parser<'a> {
// 构造函数
fn new(input: &'a str) -> Self {
Parser {
input,
position: 0,
}
}
// 解析下一个单词
// ⭐ 返回的是 &'a str(绑定在 input 上),而不是 &self,
// 所以返回之后 self 的借用就结束了,调用方可以接着 &mut self。
fn next_word(&mut self) -> Option<&'a str> {
let bytes = self.input.as_bytes();
// 第一步:跳过前导空白(少了这一步,第二次调用就会立刻返回 None)
while self.position < bytes.len() && bytes[self.position].is_ascii_whitespace() {
self.position += 1;
}
// 第二步:扫到下一个空白为止
let start = self.position;
while self.position < bytes.len() && !bytes[self.position].is_ascii_whitespace() {
self.position += 1;
}
if start == self.position {
None // 已经到结尾了
} else {
Some(&self.input[start..self.position])
}
}
// 重置解析器
fn reset(&mut self) {
self.position = 0;
}
}
fn main() {
let text = "hello world rust";
let mut parser = Parser::new(text);
println!("第一个词:{:?}", parser.next_word()); // 第一个词:Some("hello")
println!("第二个词:{:?}", parser.next_word()); // 第二个词:Some("world")
println!("第三个词:{:?}", parser.next_word()); // 第三个词:Some("rust")
println!("第四个词:{:?}", parser.next_word()); // 第四个词:None
parser.reset();
println!("重置后第一个词:{:?}", parser.next_word()); // 重置后第一个词:Some("hello")
}
|
小贴士:impl<'a> Parser<'a> 中的 'a 是泛型生命周期参数,它告诉 Rust:所有使用 'a 的地方都必须是同一个生命周期。这就像在说"这整个 Parser 实例和它的输入字符串是绑在一起的"。
9.3 生命周期与 Trait
9.3.1 Trait 定义中的生命周期
9.3.1.1 trait Foo<‘a> { fn bar(&‘a str); }
Trait 也可以有生命周期参数!这在设计一些需要引用参数的 API 时非常有用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // 定义一个带生命周期参数的 trait
trait Printable<'a> {
fn print_content(&self, content: &'a str);
}
// 为 i32 实现这个 trait
impl<'a> Printable<'a> for i32 {
fn print_content(&self, content: &'a str) {
println!("数字 {} 说:{}", self, content); // 数字 42 说:Hello, Rust!
}
}
fn main() {
let num: i32 = 42;
num.print_content("Hello, Rust!");
}
|
9.3.1.2 带生命周期的 trait 参数
Trait 的方法参数和返回值都可以包含生命周期。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| trait Runner {
// 返回值的生命周期跟 self 绑定
fn get_name(&self) -> &str;
}
struct Athlete {
name: String,
}
impl Runner for Athlete {
fn get_name(&self) -> &str {
&self.name
}
}
fn main() {
let athlete = Athlete {
name: String::from("博尔特"),
};
println!("运动员名字:{}", athlete.get_name()); // 运动员名字:博尔特
}
|
9.3.1.3 impl<‘a> Foo<‘a> for Type
实现带生命周期的 trait 时,需要在 impl 声明中带上生命周期参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| trait Formatter<'a> {
fn format(&self, input: &'a str) -> String;
}
struct Uppercase;
impl<'a> Formatter<'a> for Uppercase {
fn format(&self, input: &'a str) -> String {
input.to_uppercase()
}
}
struct Reverse;
impl<'a> Formatter<'a> for Reverse {
fn format(&self, input: &'a str) -> String {
input.chars().rev().collect()
}
}
fn main() {
let upper = Uppercase;
let reverse = Reverse;
let text = "hello world";
println!("大写:{}", upper.format(text)); // 大写:HELLO WORLD
println!("反转:{}", reverse.format(text)); // 反转:dlrow olleh
}
|
9.3.2 ‘static 生命周期的特殊含义
9.3.2.1 &‘static str(字符串字面量,生命周期整个程序期间)
static 是 Rust 中最"长寿"的生命周期,它贯穿整个程序的运行期间。
1
2
3
4
5
| fn main() {
// 字符串字面量是 'static 的,因为它们被硬编码到二进制里
let s: &'static str = "我是在程序诞生时就存在的!";
println!("{}", s); // 我是在程序诞生时就存在的!
}
|
所有的字符串字面量(用双引号括起来的)都是 'static 生命周期的,因为它们直接存储在你的程序二进制文件中,程序运行多久,它们就活多久。
9.3.2.2 T: ‘static 约束(不包含任何非 ‘static 引用)
当你在泛型上使用 T: 'static 约束时,你是在告诉编译器:“T 这个类型里不能有任何活得比程序短的引用”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // 这个函数接受任何不包含短生命周期引用的类型
fn print_static<T>(value: T)
where
T: std::fmt::Debug + 'static,
{
println!("{:?}", value);
}
fn main() {
// OK:i32 是 'static 的
print_static(42_i32); // 42
// OK:String 拥有自己的数据,不包含短生命周期引用
print_static(String::from("hello")); // "hello"
// 错误:&str 可能是非 'static 的(如果是来自局部 String 的引用)
// let local = String::from("local");
// print_static(&local); // 编译错误!
// OK:字符串字面量是 'static 的
print_static(&"可以,因为是字面量"); // "可以,因为是字面量"
}
|
9.3.2.3 ‘static 与泛型的关系
T: 'static 通常跟其他约束一起使用,来表达"这个类型必须是完全自包含的,不能引用任何外部数据"。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| use std::fmt::Display;
fn print_if_static<T>(value: &T)
where
T: Display + 'static,
{
println!("这是一个 'static 类型:{}", value);
}
fn main() {
print_if_static(&42); // 这是一个 'static 类型:42
print_if_static(&"字符串字面量"); // 这是一个 'static 类型:字符串字面量
// 注意:如果你传入一个局部变量的引用,编译会失败
// let s = String::from("hello");
// print_if_static(&s); // 错误!因为 &s 不是 'static
}
|
记忆技巧:把 'static 想象成程序员的"铁饭碗"——只要程序还在运行,这个引用就一定还在。字符串字面量就是铁饭碗持有者,而运行时创建的 String 的引用是合同工,作用域结束就"被优化"了。
9.4 高级生命周期主题
9.4.1 PhantomData 与所有权跟踪
9.4.1.1 PhantomData 的作用(标记所有权关系)
PhantomData<T> 是 Rust 中的一个"幽灵"类型——它不占用任何实际空间,但它能帮编译器理解那些"看不见"的所有权关系。
1
2
3
4
5
6
7
8
9
10
11
12
13
| use std::marker::PhantomData;
// 这是一个"拥有" T 的结构体,但实际上 T 并不被存储
// PhantomData<T> 告诉编译器:"把这个结构体当作拥有 T 来对待"
struct Owned<T> {
_marker: PhantomData<T>, // _marker 下划线前缀表示"不会被使用"
}
fn main() {
let _owned_i32 = Owned::<i32> { _marker: PhantomData };
let _owned_string = Owned::<String> { _marker: PhantomData };
println!("幽灵数据创建成功!");
}
|
PhantomData 的主要作用是:
- 让结构体"假装"拥有 T:即使 T 没有被实际存储,编译器也会认为这个结构体拥有 T 的所有权,这会影响 Drop 检查。
- 影响 Drop 检查:如果 T 实现了
Drop,那么包含 PhantomData<T> 的结构体会被视为"间接"拥有 T。 - 影响借用检查:
PhantomData<&'a T> 会让编译器认为结构体"间接"持有一个 &'a T 的引用,这在自引用结构中很有用。
9.4.1.2 泛型所有权的标记
PhantomData 在编写一些底层数据结构时特别有用,比如自定义的智能指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| use std::marker::PhantomData;
// 自定义 Box,只用于演示 PhantomData 的用法
struct MyBox<T> {
data: *mut T, // 裸指针,不受借用检查器约束
_marker: PhantomData<T>, // 标记我们"拥有" T
}
impl<T> MyBox<T> {
fn new(value: T) -> Self {
MyBox {
data: Box::into_raw(Box::new(value)),
_marker: PhantomData,
}
}
fn as_ref(&self) -> &T {
// 安全:因为 MyBox 拥有 T 的所有权,所以我们可以解引用
unsafe { &*self.data }
}
}
impl<T> Drop for MyBox<T> {
fn drop(&mut self) {
// 安全释放内存
// ⭐ 用 drop(...) 包一层而不是直接丢弃返回值:
// Box::from_raw 的返回值带 #[must_use],
// 直接写 `Box::from_raw(self.data);` 虽然也会释放内存,
// 但编译器会给出 unused_must_use 警告。
unsafe {
drop(Box::from_raw(self.data));
}
}
}
fn main() {
let box_i32 = MyBox::new(42);
println!("MyBox 里的值:{}", box_i32.as_ref()); // MyBox 里的值:42
// drop(box_i32) 会在作用域结束时自动调用 Drop
println!("作用域结束,MyBox 被正确 drop");
}
|
9.4.1.3 结构体中的所有权语义
PhantomData 还能帮助我们表达更复杂的所有权语义。比如,如果你想表达"这个结构体拥有一个指向 T 的引用",你可以使用 PhantomData<&'a T>。
1
2
3
4
5
6
7
8
9
10
11
12
| use std::marker::PhantomData;
struct RefOwner<'a, T> {
_marker: PhantomData<&'a T>, // 标记我们持有一个 &T
}
fn main() {
let value = 42;
let owner = RefOwner::<i32> { _marker: PhantomData };
println!("RefOwner 创建成功");
println!("值还在:{}", value); // 值还在:42
}
|
面试常问:为什么 PhantomData 要用下划线前缀?答:因为 PhantomData 类型的字段本身永远不会被读取(它是"幽灵"),下划线前缀告诉编译器"我知道这个字段没被使用,别警告我"。
9.4.2 NLL(Non-Lexical Lifetimes)
9.4.2.1 NLL 的改进(借用区域从"声明处到作用域末尾"缩短为"声明处到最后一次使用处")
NLL,全称 Non-Lexical Lifetimes(非词法生命周期),是 Rust 借用检查器的一次重大升级。
在 NLL 出现之前,Rust 的借用规则是"词法的"——一个引用的生命周期从它被创建的地方开始,到它所在的作用域结束时终止。这意味着,如果你写了:
1
2
3
4
| let mut v = vec![1, 2, 3];
let first = &v[0]; // 借用开始
println!("{}", first); // 使用引用
v.push(4); // 这里会报错,即使 first 后面不再使用了
|
在 NLL 出现之前,编译器会认为 first 的生命周期持续到作用域结束,所以 v.push(4) 是不允许的。但有了 NLL 之后,编译器会分析出 first 实际上只在这个 println! 里使用,之后就可以安全地修改 v 了。
1
2
3
4
5
6
7
| fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0]; // 借用开始
println!("{}", first); // 使用引用
v.push(4); // NLL: 编译器知道 first 在这之后不再使用,所以允许
println!("vec 现在是:{:?}", v); // vec 现在是:[1, 2, 3, 4]
}
|
9.4.2.2 NLL vs 传统借用检查(更精确的借用区域)
NLL 的工作原理是:从引用的使用处向后分析,找出引用的有效区域。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| fn main() {
let mut map = std::collections::HashMap::new();
map.insert("a", 1);
// 在 NLL 之前,这里的借用会持续到作用域结束
// 在 NLL 之后,编译器知道 get 返回的引用只在这个 if 块里使用
if let Some(value) = map.get("a") {
println!("找到了:{}", value); // 找到了:1
} // 借用在这里结束
// 所以这里可以继续修改 map
map.insert("b", 2);
println!("map: {:?}", map); // map: {"a": 1, "b": 2}
}
|
小剧场:想象你借了一本书,你跟图书馆说"我借到期末考试结束就还"。传统做法是你整个学期都得揣着这本书,即使你考完试早就看完了。NLL 就是图书馆聪明了一点,它会追踪你实际用这本书的时间段——你考完最后一门就自动标记为"可以还了",不需要你特意声明。
📌 一个精确的说法:NLL 并不是"运行时追踪",而是编译期做数据流分析——
从每个变量的所有使用点出发,反推出"这个借用必须存活的最小区间"(liveness 分析),
而不是简单地取"从声明到作用域结束"这个语法区间。
这也是它被叫作"非词法"(non-lexical)的原因:借用边界不再由花括号的位置决定。
⚠️ 另外要注意:NLL 解决的是引用与借用的检查,
对于 Rc/RefCell 这类"运行时才检查"的类型它管不着;它们的借用规则在运行时才会 panic。
9.4.3 Polonius 项目
9.4.3.1 Polonius 的设计目标(更宽松的所有权规则)
Polonius 是 Rust 团队正在开发的一个新的借用检查器实现。它的目标是:在保持内存安全的前提下,让借用规则更宽松一点,减少一些"过度保守"的编译错误。
当前的借用检查器有时候会拒绝一些实际上安全的代码。最经典的就是"在函数里查表,没有就插入再返回"这个模式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| use std::collections::HashMap;
// 想要:有就返回已有值的可变引用,没有就插入默认值再返回它
fn get_or_insert<'r, K, V>(
map: &'r mut HashMap<K, V>,
key: K,
default: V,
) -> &'r mut V
where
K: std::hash::Hash + Eq + Clone,
{
match map.get_mut(&key) {
Some(v) => v,
None => {
// ❌ 这里会报错:map 已经被上面的 get_mut 借走了
map.insert(key.clone(), default);
map.get_mut(&key).unwrap()
}
}
}
fn main() {}
|
当前稳定的借用检查器会给出:
error[E0499]: cannot borrow `*map` as mutable more than once at a time
--> src/main.rs:14:13
|
3 | fn get_or_insert<'r, K, V>(
| -- lifetime `'r` defined here
...
11 | match map.get_mut(&key) {
| - --- first mutable borrow occurs here
| _____|
| |
12 | | Some(v) => v,
13 | | None => {
14 | | map.insert(key.clone(), default);
| | ^^^ second mutable borrow occurs here
... |
17 | | }
| |_____- returning this value requires that `*map` is borrowed for `'r`
这段代码其实是安全的:Some 分支里 v 会被直接返回,None 分支里 v(那个借用)
根本不会被用到。但 NLL 的算法在这一点上仍然偏保守——它只知道"借用可能被返回",
不敢断定 None 分支里那笔借用已经作废。
💡 顺带澄清:以前常被拿来举例的"let first = &v[0]; v.push(6);(不使用 first)"
现在的稳定版 Rust 已经能通过了,那是 NLL 早就解决的场景,不需要 Polonius。
真正还需要 Polonius 的,是上面这种"条件返回引用"的模式。
在 Polonius 稳定之前,现实中的绕法通常是:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| use std::collections::HashMap;
fn get_or_insert<'r, K, V>(map: &'r mut HashMap<K, V>, key: K, default: V) -> &'r mut V
where
K: std::hash::Hash + Eq + Clone,
{
// 绕法一:先用不可变借用判断存在性,把"是否插入"决定完,再去做可变借用
if !map.contains_key(&key) {
map.insert(key.clone(), default);
}
map.get_mut(&key).unwrap()
// 绕法二:用 entry API(标准库专门为这个场景设计的,最推荐)
// map.entry(key).or_insert(default)
}
fn main() {
let mut m: HashMap<&str, i32> = HashMap::new();
println!("{}", get_or_insert(&mut m, "a", 1)); // 1
println!("{}", get_or_insert(&mut m, "a", 99)); // 1(已存在,不会被覆盖)
}
|
9.4.3.2 基于租借(Loan)的分析模型
Polonius 使用一种叫做"基于租借(Loan)的分析模型"。它不再把借用想成"一个变量持有另一个变量的引用",而是把它想成"一块数据被租借出去了,租借到期就可以归还"。
graph TD
A["变量 v: Vec"] -->|租借出| B["Loan 1: &v[0]"]
A -->|可变借用| C["Loan 2: push 操作"]
B -->|归还| A
C -->|完成| A
A -->|使用| D["println 输出"]在 Polonius 模型中,每个 Loan 都有自己的生命周期,只有当所有活跃的 Loan 都"归还"之后,原变量才能被修改或重新借用。
9.4.3.3 Polonius 的开发状态
Polonius 目前还在开发中,预计会在未来的 Rust 版本中作为替代性的借用检查器。你可以通过 -Zpolonius 标志来启用它进行测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 注意:Polonius 还在开发中,你需要使用 nightly 版本的 Rust
// 并通过 -Z 参数(只有 nightly 才接受)来启用它
// ⚠️ 注意 -Zpolonius 是传给 rustc 的,不是 cargo 的子命令参数。
// 正确做法是通过 RUSTFLAGS 转发:
// RUSTFLAGS="-Zpolonius" cargo +nightly build
//
// 或者直接调用 cargo rustc 把参数交给 rustc:
// cargo +nightly rustc -- -Zpolonius
fn main() {
println!("等待 Polonius 稳定...");
}
|
预告:Polonius 的引入可能会让一些目前需要 unsafe 或者 RefCell 来绕过的代码变得可以直接用安全代码实现。期待那一天的到来!
本章小结
这一章我们深入探索了 Rust 的生命周期系统。以下是关键知识点:
- 生命周期标注:用
'a 这样的语法告诉编译器引用的"保质期" - 省略规则:一共三条——①每个省略的输入生命周期各自成为独立参数;②输入侧只有一个生命周期时,输出取它;③有
&self/&mut self 时,输出取 self 的 - 生命周期约束:
T: 'a 意味着类型 T 中所有引用都不能比 'a 短(注意 'a 必须先声明,否则报 E0261) - 生命周期子类型:
'a: 'b 表示 'a 至少要活得跟 'b 一样久;因此 &'a T 可以当成 &'b T 用(长命可以当短命用,方向别记反) - 结构体与生命周期:包含引用的结构体必须标注生命周期
- Trait 与生命周期:Trait 可以有生命周期参数,实现时也需要声明
'static:程序运行期间一直存在的引用,字符串字面量就是 'static- PhantomData:用来标记"看不见"的所有权关系
- NLL:非词法生命周期,让借用检查更精确
- Polonius:未来的新一代借用检查器,会让规则更宽松
几个容易踩的坑(本章示例已经全部用 rustc 实测过)
- 返回引用的生命周期是"编译期承诺",不是运行时事实:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str 即使实际上总是返回 x,只要签名这么写,缩短 y 的寿命就会让返回值失效(E0597)。想让返回值只跟 x 绑定,就把 y 的生命周期单独写出来。 &s[..1] 不是"取第一个字符":空字符串会 panic,多字节字符(如"你")会因为不在字符边界上 panic。按字符取请用 char_indices() 或 chars().next()。- 多参数且没有
self 时,返回引用必须显式标注生命周期,否则报 E0106。 - 手写
Drop 时不要丢弃 Box::from_raw 的返回值,用 drop(...) 包一层,避免 unused_must_use 警告。 - 旧教程里常见的"
let first = &v[0]; v.push(6); 会报错"已经过时,这是 NLL 早就解决的问题;真正需要 Polonius 的是"条件返回引用"(如 get_or_insert)。
生命周期是 Rust 最独特的特性之一,它在编译期为你的程序加了一道强大的安全锁。虽然有时候写起来会觉得"怎么这么啰嗦",但当你的程序跑起来稳如老狗的时候,你会感谢这个啰嗦的编译器。
记住:在 Rust 的世界里,没有编译通过的借用都是耍流氓!