CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Rust 语言入门/课时/生命周期
RawBlame
课时目录12 节

Rust 语言入门

  1. 01Rust 是什么
  2. 02基础语法
  3. 03所有权与借用
  4. 04生命周期
  5. 05结构体与枚举
  6. 06错误处理
  7. 07Trait 与泛型
  8. 08集合类型
  9. 09并发与 async
  10. 10宏与元编程
  11. 11Web 与生态
  12. 12综合练习

生命周期

lifetime annotations、elision、常见模式。

35 分钟

生命周期

这一课你将能做到

  1. 解释生命周期标注的本体:它是对引用「有效范围」的描述,不是延长寿命的开关
  2. 掌握三条省略规则,判断什么时候必须显式写 'a
  3. 写出带生命周期参数的函数与结构体,读懂 E0106 报错

为什么要学

上一课的借用规则里有一条「引用不能比被引用者活得久」。大多数情况编译器能自己推断,但函数有多个引用参数、或结构体里存引用时,编译器需要你补充标注才能完成验证。

这一课在 Java 工程师视角几乎全新:Java 的引用始终有效(GC 保证对象不被提前回收),根本不存在这个概念。理解它的价值在于——你会真正明白 Rust 为什么敢在无 GC 的前提下宣称「引用永远安全」。

先建立图像

生命周期不是新东西,它一直存在,标注只是给编译器看的「范围说明」:

fn longest(x: &str, y: &str) -> &str {
//        │         │           └── 返回的引用:从 x、y 里来,能活多久?
//        │         └── y 的有效范围
//        └── x 的有效范围
//
// 编译器必须保证:返回引用的有效范围 ⊆ x 和 y 的有效范围的交集
// 'a 就是这个交集的名字
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str

类比 Java:JVM 有逃逸分析,Rust 把同样的验证搬到了类型系统里,并要求写不出来的地方明说。

跟着做

步骤 1:复现一个真实的 E0106

cargo new life-demo
cd life-demo
// fn longest(x: &str, y: &str) -> &str {
//     if x.len() > y.len() { x } else { y }
// }
// ← 取消注释:error[E0106] missing lifetime specifier
//   编译器不知道返回的引用活多久,拒绝放行

fn main() {}

报错原因:返回的引用可能来自 x 也可能来自 y,编译器无法确认它比 x、y 都活得短。

步骤 2:加上标注,让规则显式化

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

fn main() {
    let log1 = String::from("runtime panic");
    let result;
    {
        let log2 = String::from("borrow check");
        result = longest(&log1, &log2);
        println!("较长者:{result}");    // OK:result 的使用在两者都存活的区间内
    }
    println!("{result}");    // ← 取消注释:error[E0597] log2 活得不够久
}

'a 是一个范围变量:编译器会把 'a 实例化为「实际调用中两个引用寿命的交集」。把最后的 println 解开,编译器用 E0597 告诉你交集覆盖不了这个使用点——标注没有改变任何对象的真实寿命,它只是让检查可执行。

步骤 3:三条省略规则——为什么平时不用写

fn first_word(s: &str) -> &str {       // 实际签名:fn first_word<'a>(s: &'a str) -> &'a str
    s.split_whitespace().next().unwrap_or("")
}

fn main() {
    let line = String::from("hello rust world");
    let w = first_word(&line);
    println!("{w}");
}

省略规则:① 每个引用参数获得一个独立的生命周期参数;② 只有一个输入引用时,它被赋给所有输出引用;③ 方法有 &self 时,self 的生命周期赋给所有输出。规则覆盖不了的(如两个引用参数加返回引用),才要求显式标注。

步骤 4:结构体持有引用

struct Excerpt<'a> {
    part: &'a str,     // Excerpt 实例不能比它借用的文本活得久
}

impl<'a> Excerpt<'a> {
    fn len(&self) -> usize { self.part.len() }
    fn shout(&self) -> String { format!("{}!", self.part.to_uppercase()) }
}

fn main() {
    let novel = String::from("call me ishmael. some years ago");
    let first = novel.split('.').next().unwrap();
    let e = Excerpt { part: first };
    println!("{} ({} chars)", e.shout(), e.len());
}

结构体带 'a 参数的含义:这个实例的存在前提是「被引用的数据还活着」。把 novel 在 e 之前 drop 的代码会被编译器拒绝。

步骤 5:认识 'static

static MAX_USERS: u32 = 10_000;          // 全局常量,程序全程有效

fn banner() -> &'static str {
    "===== Mini Judge ====="              // 字面量编译进二进制,永远有效
}

fn main() {
    let s: &'static str = banner();
    println!("{s}  max={MAX_USERS}");
}

'static 表示整个程序运行期有效。字符串字面量都是 'static;遇到「需要 'static 但拿到短命数据」的报错,通常正确解法是改用所有权(String)而不是硬转。

讲清楚要点

  1. 标注不改变寿命,只描述寿命:'a 是编译期验证用的代数符号,运行时完全消失,零开销。
  2. 省略规则覆盖 90% 场景:一个引用进、一个引用出的函数全部免标注;两个引用参与、返回引用的函数必须写。
  3. 结构体的 'a 是存在前提:Excerpt<'a> 编译进类型系统后,「过期引用」在类型层面不可表达。
  4. 生命周期是泛型的一种:fn longest<'a> 和 fn largest<T> 同构,一个约束范围,一个约束类型。
  5. 遇到 'static 约束先想所有权:线程闭包要求 'static(第 9 课)、tokio spawn 同理,惯用解法是 move 把数据所有权搬进去,而不是延长引用。

你可能会懵的地方

  • 生命周期标注会不会让对象活得更久? 完全不会。它不生成任何代码,纯粹是给借用检查器的证明材料;对象寿命仍由所有权决定。

  • 为什么两个引用参数返回引用就必须写标注? 因为编译器推不出返回引用来自谁。'a 的作用是把「返回值与两个参数同寿命交集」这个业务事实告诉编译器——你比它懂业务,所以由你声明,由它验证。

  • 'a 和 'static 是什么关系? 'a 是范围变量,调用时被实例化为具体范围;'static 是那个最大范围(整个程序期)。&'static str 能赋给 &'a str 位置的变量,反之不行。

  • 引用计数的 Arc 也要生命周期吗? Arc 拥有数据所有权,不涉及借用,不需要标注。生命周期只管「借用来的」。需要共享又怕生命周期麻烦时,Arc 是合法逃生通道,但先想想是不是在逃避正确建模。

  • Java 有对应概念吗? 没有直接对应。Java 的逃逸分析、对象晋升与 GC 代际是运行时机制;Rust 把等价的「引用是否逃逸、是否悬垂」验证编译期完成。

错法对照

错法问题正解
给所有函数手写生命周期无意义噪音先不写,编译器要求时再补
返回局部变量的引用必然悬垂,E0515返回所有权类型,或返回 'static 字面量
结构体存引用却没写 'aerror[E0106]struct S<'a> { part: &'a str }
用 'static 硬转短命数据数据其实已经死亡改用 owned 类型(String/Box)传递所有权
混淆 'a 的位置:&'a T 写成 &T<'a>语法错误标注属于引用本身:&'a str

卡点急救

  1. error[E0106] missing lifetime specifier:编译器会列出它考虑过的省略规则;按提示补 'a,把返回引用与相关输入引用绑到同一 'a。
  2. error[E0597] borrowed value does not live long enough:某内部作用域的值被外部引用。解法按优先级:把数据提到外层作用域、clone 一份、改为传所有权。
  3. 「expected lifetime 'a, found 'static」来回打架:通常是结构体/函数签名里把范围定死成 'a,调用处传了更长或更短的数据;先统一签名里所有引用用同一个 'a,再让具体调用去实例化。

面试常问

  • 生命周期标注解决什么问题? 在无 GC 的前提下,让编译器机械地验证「所有引用都不会比被引用者活得久」,把悬垂指针这类 C/C++ 的运行时灾难变成编译错误,且运行时零开销。

  • 为什么大部分函数不需要写生命周期? 省略规则三条(参数各占一个、单入参赋给输出、self 优先)能确定性地推导绝大多数签名;需要显式标注的场景本质是「编译器无法从签名推断返回引用的来源」。

  • 'static 在什么场景出现? 字符串字面量与全局常量;Box::leak 之类的泄漏;以及线程/tokio spawn 的闭包约束——此时惯用 move 语义把所有权搬进闭包来满足。

自测三问

  1. 'a 在 fn longest<'a>(x: &'a str, y: &'a str) -> &'a str 里被实例化成什么?
  2. 三条省略规则分别是什么?举一个规则覆盖不了必须手写的签名。
  3. struct Excerpt<'a> { part: &'a str } 中 'a 对 Excerpt 实例的寿命意味着什么?

小结一句话

生命周期标注是写给借用检查器的范围代数:你声明业务事实,它机械验证,运行时零成本——这是 Rust 无 GC 又无悬垂引用的最后一环。

延伸

  • The Rust Book ch10.3:生命周期语法
  • 下一课:结构体与枚举——用类型建模评测结果,match 模式匹配登场。
加载本节练习…
← 上一课所有权与借用下一课 →结构体与枚举