阶段一 · 生命周期与引用

生命周期进阶

一句话总结

多数函数不用写生命周期, 是因为编译器有一套**省略规则(elision rules)**替你补全.
当结构体要持有引用时, 必须给结构体标注生命周期--这等于声明"该结构体不能比它借用的数据活得久".
'static 则表示"活整个程序", 别被它的字面意思误导.

省略规则: 为什么大多数时候不用写

上一篇说"涉及引用的函数要标生命周期", 但实际写过的大量带引用函数其实都没标过. 原因是编译器内置了三条省略规则, 能覆盖绝大多数常见情形, 自动补全标注:

  1. 每个引用参数各自分配一个生命周期参数.
  2. 只有一个输入生命周期, 它被赋给所有输出引用.
  3. 若有 &self&mut self(即方法), self 的生命周期被赋给所有输出引用.
// 写的:
fn first_word(s: &str) -> &str { ... }
// 编译器按规则①②补全为:
fn first_word<'a>(s: &'a str) -> &'a str { ... }
// 只有一个输入引用, 输出自动绑定它 -- 无歧义, 无需动手

为什么 longest 不能被省略

回看上一篇的 longest(x: &str, y: &str): 它有两个输入引用, 规则②(单输入)不适用, 规则③(方法)也不适用--编译器无法确定输出该绑哪个输入, 省略规则失效, 于是要求手动标注.

记住: 需要写标注的, 正是省略规则覆盖不到的"有歧义"场景.

省略规则像表单的"智能默认值": 单个引用输入时, 输出"显然"借的就是它, 编译器替你填好; 多个引用时它不敢猜, 才把空格留给程序员.
所以感觉"大部分函数不用写"--不是不需要, 是被默默填上了.

结构体持有引用: 必须标注

当结构体的字段是引用而非拥有的值时, 必须为结构体声明生命周期. 这是进阶篇真正高频用到标注的地方:

// 这个结构体借用了一段字符串, 而非拥有它
struct Excerpt<'a> {
part: &'a str,
}

let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().unwrap();
let e = Excerpt { part: first_sentence };
// e 不能比 novel 活得久 -- 'a 声明的正是这条约束

struct Excerpt<'a> 读作: "Excerpt 实例的存活时间, 不能超过它 part 字段所借用的数据." 这迫使编译器在任何使用 Excerpt 的地方, 都确保被借的字符串还活着.

设计抉择: 借用还是拥有

结构体持有引用(&'a str)能省去复制, 但带来生命周期约束--它会"绑住"被借的数据, 使用处处受 'a 牵制.

很多时候, 让结构体拥有数据(用 String 而非 &str)反而更省心. "持有引用的结构体"是把双刃剑: 性能换来约束. 初学阶段, 除非确有必要, 优先让结构体拥有数据.

方法中的生命周期

给带生命周期的结构体写方法, 语法上要在 impl 后声明 'a(和泛型参数一样). 但得益于省略规则③, 方法体内多数时候不用重复标注:

impl<'a> Excerpt<'a> {
// 返回 &str, 按规则③自动绑定到 &self 的生命周期, 无需手写
fn announce(&self, msg: &str) -> &str {
println!("注意: {msg}");
self.part
}
}

'static: 活整个程序

'static 是一个特殊的, 预定义的生命周期, 表示"该引用在整个程序运行期间都有效". 最典型的 'static 数据是字符串字面量--它们被编入二进制, 从程序启动到结束一直存在:

let s: &'static str = "我活得和程序一样久"; // 字面量天然是 'static

两个关于 'static 的常见误解

误解一: 看到报错让我加 'static, 我就加. 编译器建议加 'static 往往不是真正的解法--它可能在提示"这个引用活得不够久", 盲目加 'static 只会把错误推到别处. 先想清楚数据到底活多久.

误解二: T: 'static 约束 = 必须是 'static 数据.'static 作为泛型约束出现(如 T: 'static), 它的意思是"T 不包含任何非 'static 的借用"--一个拥有所有权的 String 就满足 T: 'static, 因为它不借用任何东西. 这与"引用要活整个程序"是两回事. 在「异步 Rust」分类会反复遇到这个约束.

快速回顾

  • 省略规则: 三条规则(逐参数分配, 单输入贯通, self 优先)替你补全标注, 故多数函数不用写.
  • 何时必须写: 省略规则覆盖不到的歧义场景--典型是多个引用输入且返回引用(如 longest).
  • 结构体持有引用: 必须标 struct S<'a>, 声明"实例不能比所借数据活得久"; 常可改为拥有数据来回避.
  • 方法: impl<'a> S<'a> 声明, 方法体多由规则③自动绑定 self, 无需重复标.
  • 'static: 表示活整个程序(如字面量); 作为泛型约束 T: 'static 意为"不含非 'static 借用", 拥有所有权的值即满足.

动手练习

  1. 这个函数需要手写生命周期吗? 用省略规则解释.

    fn trim_prefix(s: &str, n: usize) -> &str { &s[n..] }
参考答案

不需要. 只有一个引用输入 s(n 不是引用), 命中省略规则②: 单个输入生命周期被赋给输出. 编译器自动补全为 fn trim_prefix<'a>(s: &'a str, n: usize) -> &'a str, 无歧义, 无需手写.

  1. 下面的结构体定义为什么编译不过? 怎么修?

    struct Parser {
    input: &str,
    }
参考答案

结构体字段是引用 &str, 但没有声明生命周期, 编译器不知道这个引用(以及持有它的结构体)能活多久. 报 "missing lifetime specifier". 修法:

struct Parser<'a> {
input: &'a str,
}

若不想被生命周期约束, 也可改为拥有数据: input: String. 两种选择对应"借用 vs 拥有"的设计权衡.

  1. 判断真假: "给一个函数参数加上 'static, 能让传入的临时变量活得更久."
参考答案

假. 生命周期标注是描述性的, 'static 也不例外--它不会延长任何数据的实际寿命. 给参数标 &'static T 反而是施加约束: 要求调用方传入的引用本身就活整个程序, 否则编译失败. 它是"提条件", 不是"给能力".

  1. 一个拥有所有权的 String 满足泛型约束 T: 'static 吗? 为什么?
参考答案

满足. T: 'static 的含义是"T 不包含任何非 'static 的借用", 而不是"T 必须活整个程序". 一个 String 拥有自己的数据, 不借用任何外部引用, 因此满足 T: 'static.

这个约束在把数据交给线程或异步任务(它们可能活得很久)时很常见--要求传入的东西"自给自足, 不依赖外部借用".