第 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 的主要作用是:

  1. 让结构体"假装"拥有 T:即使 T 没有被实际存储,编译器也会认为这个结构体拥有 T 的所有权,这会影响 Drop 检查。
  2. 影响 Drop 检查:如果 T 实现了 Drop,那么包含 PhantomData<T> 的结构体会被视为"间接"拥有 T。
  3. 影响借用检查: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 的生命周期系统。以下是关键知识点:

  1. 生命周期标注:用 'a 这样的语法告诉编译器引用的"保质期"
  2. 省略规则:一共三条——①每个省略的输入生命周期各自成为独立参数;②输入侧只有一个生命周期时,输出取它;③有 &self/&mut self 时,输出取 self 的
  3. 生命周期约束:T: 'a 意味着类型 T 中所有引用都不能比 'a 短(注意 'a 必须先声明,否则报 E0261)
  4. 生命周期子类型:'a: 'b 表示 'a 至少要活得跟 'b 一样久;因此 &'a T 可以当成 &'b T 用(长命可以当短命用,方向别记反)
  5. 结构体与生命周期:包含引用的结构体必须标注生命周期
  6. Trait 与生命周期:Trait 可以有生命周期参数,实现时也需要声明
  7. 'static:程序运行期间一直存在的引用,字符串字面量就是 'static
  8. PhantomData:用来标记"看不见"的所有权关系
  9. NLL:非词法生命周期,让借用检查更精确
  10. 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 的世界里,没有编译通过的借用都是耍流氓!

最后修改 September 19, 2026: 更新 (3489033b1)