Rust: Rust 生命周期(Lifetimes):引用的有效期标注

最后更新:2026-08-26

生命周期(Lifetime)是 Rust 确保"引用永远不会悬垂"的机制——它给每个引用贴上了"有效期"标签。

生命周期你其实已经在用了——第 9 课和第 10 课的所有代码都有生命周期,只是编译器帮你"猜"出来了。本课教你什么时候需要手动标注生命周期。


1. 你将学到


2. 概念图解

100%
flowchart LR
    subgraph "函数签名 fn longest<'a>"
        X["x: &'a str"] --> RET["返回值: &'a str"]
        Y["y: &'a str"] --> RET
    end
    NOTE["'a = x 和 y 中较短者的生命周期"] -.-> RET

3. 一个追踪老鼠的故事

(1) 痛苦:老鼠跑了还在看

Tom 是一名生物学家,他在用一个追踪器记录老鼠的位置:

TEXT 📖 仅展示
追踪器锁定老鼠 A → 老鼠 A 跑进了洞里 → 追踪器还在指向洞口

"如果追踪器上有个标签写着'有效期限:到老鼠 A 出洞为止'就好了……"

(2) Rust 生命周期的解决方案

RUST
// 生命周期标注 'a:告诉编译器"返回值的有效期不超过参数 x 的有效期"
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let string1 = String::from("长字符串");
    let result;

    {
        let string2 = String::from("短");
        result = longest(&string1, &string2);  // ✅ string2 和 string1 都还活着
        println!("较长的是: {}", result);
    }  // string2 在这里被销毁

    // println!("{}", result);  // ❌ 如果在这里打印,result 引用的 string2 已销毁
}  // string1 在这里被销毁

生命周期标注就像给追踪器贴了一个"有效期"标签。编译器根据这个标签判断引用是否还在有效期内。在这个例子中,result 的生命周期不能超过 string2 的生命周期。


4. 生命周期工作原理

(1) 每个引用都有生命周期

RUST
fn main() {
    let x: i32 = 10;           // x 的生命周期从这里开始
    let r: &i32 = &x;          // r 的生命周期从这里开始
    println!("r: {}", r);       // 使用 r
}                               // r 的生命周期先结束,然后 x 的生命周期结束

(2) 为什么需要标注生命周期?

当函数返回一个引用时,编译器需要知道这个引用来自哪个参数:

100%
graph TB
    A[函数返回引用] --> B{返回值指向哪个参数?}
    B --> C[指向参数1 → 返回值的生命周期 ≤ 参数1]
    B --> D[指向参数2 → 返回值的生命周期 ≤ 参数2]
    B --> E[指向两个中的某一个 → 必须标注: 取交集]
    C --> F[无需标注(编译器能推断)]
    D --> F
    E --> G[需要手动标注 'a]
场景 例子 是否需要标注
单个输入引用 fn first(x: &str) -> &str 不需要(省略规则)
多个输入引用 fn longest(x: &str, y: &str) -> &str 需要(编译器不知选谁)
不返回引用 fn len(x: &str) -> usize 不需要
结构体含引用 struct S<'a> { r: &'a i32 } 需要

(3) 生命周期标注位置速查

位置 语法 说明
函数定义 fn foo<'a>(x: &'a str) -> &'a str 连接参数和返回值
结构体定义 struct S<'a> { r: &'a str } 结构体不能比引用活得更久
impl 块 impl<'a> S<'a> 实现方法时声明生命周期
trait 约束 where T: 'a 约束类型不能含短命引用
静态标注 &'static str 整个程序运行期间有效

5. 生命周期标注

(1) 语法

RUST
// 'a 是生命周期参数的名称(习惯用 'a、'b、'c)
// 读法:对于生命周期 'a,x 和 y 都至少需要活 'a 这么久

fn function<'a>(x: &'a str, y: &'a str) -> &'a str {
    // 返回值的生命周期 = x 和 y 中较短的那个
}

(2) 生命周期省略规则

编译器在以下三种情况下可以自动推断生命周期(不需要你写):

规则 含义
每个输入引用都有各自的生命周期 fn foo(x: &str) 自动给 x 一个生命周期
只有一个输入引用 输出引用的生命周期 = 输入引用的生命周期
多个输入,其中一个是 &self 或 &mut self 输出引用生命周期 = &self 的生命周期
RUST
// 不需要标注:只有一个输入引用
fn first_word(s: &str) -> &str { &s[..] }

// 需要标注:两个输入引用
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

6. 生命周期示例

▶ 示例 1:为什么需要生命周期(难度 ⭐⭐⭐)

RUST
// ============================================
// 没有生命周期标注会怎样?
// ============================================

// 这个函数有两个输入引用,返回值可能是其中任何一个
// ❌ 没有标注会编译报错:expected lifetime parameter
// fn longest_wrong(x: &str, y: &str) -> &str {
//     if x.len() > y.len() { x } else { y }
// }

// ✅ 正确版本:标注生命周期
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("hello");
    let result;

    {
        let s2 = String::from("hi");
        result = longest(&s1, &s2);  // result 的生命周期 = s2 的生命周期(较短的那个)
        println!("较长的是: {}", result);
    }  // s2 被销毁,result 无效

    // println!("{}", result);  // ❌ 编译器会阻止:result 的生命周期已经结束
}

输出:

TEXT 📖 仅展示
较长的是: hello

生命周期 'a 取的是 xy较短的那个。如果返回值的生命周期设为长的那一个,它可能在短的那个销毁后仍然被使用——这就是编译器要防止的。


▶ 示例 2:不同生命周期的参数(难度 ⭐⭐⭐)

RUST
// ============================================
// 两个参数有不同生命周期
// ============================================

// 两个生命周期:'a 对应 x,'b 对应 y
// 返回值只与 x 有关,所以用 'a
fn choose_first<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
    x  // 只返回 x,所以生命周期只需要 'a
}

fn main() {
    let x = String::from("长命百岁");

    let result;
    {
        let y = String::from("短命鬼");
        result = choose_first(&x, &y);  // ✅ result 只依赖于 x
        println!("result: {}", result);
    }  // y 销毁,但 result 不依赖 y,所以没问题

    println!("离开内层后仍然有效: {}", result);  // ✅ x 还活着
}

输出:

TEXT 📖 仅展示
result: 长命百岁
离开内层后仍然有效: 长命百岁

当返回值只依赖于某个参数时,只需标注该参数的生命周期。choose_firsty 参数即使短命也不影响结果——因为返回值根本没用到 y 的数据。


▶ 示例 3:结构体中的生命周期(难度 ⭐⭐⭐⭐)

RUST
// ============================================
// 结构体里存放引用——必须标注生命周期
// ============================================

struct Excerpt<'a> {
    content: &'a str,  // 结构体借用外部的字符串
}

impl<'a> Excerpt<'a> {
    fn length(&self) -> usize {
        self.content.len()
    }

    fn announce(&self, announcement: &str) -> &str {
        println!("公告: {}", announcement);
        self.content  // 返回结构体内部的引用
    }
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("找不到句号");

    let excerpt = Excerpt {
        content: &first_sentence,
    };

    println!("摘录内容: {}", excerpt.content);
    println!("长度: {}", excerpt.length());
    println!("公告结果: {}", excerpt.announce("欢迎阅读"));
}  // excerpt 必须先于 novel 销毁(内容依赖 novel)

输出:

TEXT 📖 仅展示
摘录内容: Call me Ishmael
长度: 15
公告结果: Call me Ishmael

结构体里存放引用时,必须在结构体名字上标注生命周期参数 <'a>。这告诉编译器:结构体实例的生命周期不能超过它引用的数据的生命周期。


▶ 示例 4:'static 生命周期(难度 ⭐⭐⭐)

RUST
// ============================================
// 'static:引用在整个程序运行期间有效
// ============================================

fn main() {
    // 字符串字面量是 'static 的
    let s: &'static str = "我活在程序的整个生命周期里";
    println!("{}", s);

    // 'static 约束:要求 T 不能包含任何非静态引用
    fn print_static<T: 'static>(val: &T) {
        println!("{:?}", std::ptr::from_ref(val));
    }

    let num: i32 = 42;
    print_static(&num);  // ✅ i32 不包含引用,符合 'static

    // let msg = String::from("temp");
    // let r = &msg;
    // print_static(&r);  // ❌ r 引用了非 'static 的 msg
}

输出:

TEXT 📖 仅展示
我活在程序的整个生命周期里
0x...(内存地址)

'static 是 Rust 中最长的生命周期——数据在程序启动时就存在,直到程序结束才销毁。字符串字面量就是 'static 的。请注意:'static 应该被视为"这个引用永远有效",而不是"这个数据一直活到最后"。


▶ 示例 5:综合练习——文本分析器(难度 ⭐⭐⭐)

RUST
// ============================================
// 生命周期实战:零拷贝文本分析
// ============================================

struct TextAnalyzer<'a> {
    text: &'a str,
}

impl<'a> TextAnalyzer<'a> {
    fn new(text: &'a str) -> Self {
        TextAnalyzer { text }
    }

    fn word_count(&self) -> usize {
        self.text.split_whitespace().count()
    }

    fn longest_word(&self) -> &'a str {
        self.text
            .split_whitespace()
            .max_by_key(|w| w.len())
            .unwrap_or("")
    }

    fn first_n_words(&self, n: usize) -> Vec<&'a str> {
        self.text.split_whitespace().take(n).collect()
    }

    fn line_count(&self) -> usize {
        self.text.lines().count()
    }

    fn char_count(&self) -> usize {
        self.text.chars().count()
    }

    fn summary(&self) -> String {
        format!(
            "字符: {}, 单词: {}, 行数: {}, 最长词: '{}'",
            self.char_count(),
            self.word_count(),
            self.line_count(),
            self.longest_word()
        )
    }
}

fn highlight_word<'a>(text: &'a str, word: &str) -> Vec<&'a str> {
    text.split_whitespace()
        .filter(|w| w.contains(word))
        .collect()
}

fn main() {
    let article = "Rust is a systems programming language \
that runs blazingly fast and prevents segfaults. \
Rust guarantees memory safety and thread safety.";

    let analyzer = TextAnalyzer::new(article);
    println!("=== 文本分析 ===");
    println!("{}", analyzer.summary());

    let top3 = analyzer.first_n_words(3);
    println!("\n前 3 个词: {:?}", top3);

    let rust_mentions = highlight_word(article, "Rust");
    println!("包含 'Rust' 的词: {:?}", rust_mentions);

    let paragraph = "First line.\nSecond line.\nThird line.";
    let p_analyzer = TextAnalyzer::new(paragraph);
    println!("\n段落分析: {}", p_analyzer.summary());
}

输出:

TEXT 📖 仅展示
=== 文本分析 ===
字符: 124, 单词: 18, 行数: 1, 最长词: 'blazingly'

前 3 个词: ["Rust", "is", "a"]
包含 'Rust' 的词: ["Rust", "Rust"]

段落分析: 字符: 39, 单词: 6, 行数: 3, 最长词: 'Second'

TextAnalyzer 通过生命周期 <'a> 持有对原始文本的引用——所有分析方法都是零拷贝的。longest_word 返回的 &'a str 指向原始文本中的单词切片,不会产生任何内存分配。


❓ 常见问题

Q 生命周期是运行时概念吗?
A 不是!生命周期完全是编译时概念。
Q 'a 中的 a 是什么意思?
A 只是一个名字,习惯用 a、b、c……
Q 为什么大部分 Rust 代码看不到生命周期标注?
A 因为有生命周期省略规则让编译器帮我们猜。
Q 输入引用的生命周期 'a 和输出来回 'a 是什么关系?
A 它们取交集。
Q 我什么时候需要自己写生命周期?
A 只有 3 种情况:函数有多个输入引用并返回引用、结构体里存引用、实现 trait 时约束关联类型。

📖 小节


📝 作业

  1. 难度 ⭐:阅读一段已有的 Rust 代码(比如第 9 课的例程),找出哪些函数靠生命周期省略规则避免了手动标注。
  2. 难度 ⭐⭐⭐:写一个函数 fn shortest<'a>(x: &'a str, y: &'a str) -> &'a str,返回两个字符串中较短的那个。在 main 中创建不同生命周期的变量来调用它。
  3. 难度 ⭐⭐⭐⭐:定义一个结构体 Book<'a> 包含 &'a str 类型的字段 title。写一个 impl 块,实现一个方法 fn first_word(&self) -> &str 返回标题的第一个单词。在 main 中演示它的使用。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏