所有权:编译器为什么拒绝我

生命周期标注到底在说什么

它在解决什么

借用那篇最后撞上了一个新问题:想从函数里返回一个引用, 编译器报的是 E0106: missing lifetime specifier —— 它在问「这个引用要活多久」。

这个问题对编译器来说是刚需。它要在编译期证明「这个引用被用的时候, 它指向的东西还活着」,而跨函数边界时它只能看到签名,看不到函数体和调用点。 签名里没写清楚,它就没法证明,于是拒绝。

生命周期标注就是把这件事写进签名的语法。这一篇讲三件事: 为什么大多数时候你不用写、必须写的时候那个 <'a> 在说什么、 以及一个几乎人人都会有的误解。

🚨 先破除那个误解:标注不延长任何东西

从名字上看,「生命周期标注」很像是在设定某个东西能活多久。不是。 它只是在陈述一个约束,编译器拿它去做证明。值的实际寿命由作用域决定, 和你写没写 'a 一点关系都没有。

这段代码是最直接的证据:函数签名一字未改、 'a 一个不少,照样编译不过。

let a = String::from("hello");
let result;
{
    let b = String::from("hi");
    result = longer(&a, &b);
}          // ← b 在这里没了
println!("{result}");   // E0597: `b` does not live long enough

longer 的签名说「返回值不会比 a、b 里先死的那个活得更久」。 b 在内层块结束时就死了,而 result 要活到 println! —— 约束不成立, 所以拒绝。标注没有、也不可能让 b 多活一行。

⇒ 把 <'a> 读成「至少活到」这种关系,而不是「活多久」这种时长, 后面的一切都会顺很多。对照项只是把 b 挪到和 a 同一层,一个字的标注都没加,就过了。

为什么大多数时候不用写

写 Rust 几个星期都碰不到 <'a> 是很正常的 —— 不是因为它不重要, 而是因为编译器在大多数情况下能自己推出唯一答案。这套推导叫省略规则, 只有三条:

  1. 每个被省略的输入引用,各自分配一个独立的生命周期。
  2. 如果只有一个输入生命周期,就把它赋给所有被省略的输出。
  3. 如果有多个输入,但其中一个是 &self 或 &mut self,就把 self 的赋给输出。

关键在第 2 条的「只有一个」:一个输入引用时, 编译器没得选 —— 输出只可能跟着它。而对照项把参数换成两个, 唯一性当场消失,E0106 就来了。

⚠️ 注意编译器不会去猜。longer 的函数体里明明写着「返回 a 或 b」, 但它不看函数体 —— 只看签名。这不是偷懒,是刻意的:签名是契约, 改函数体不该悄悄改变调用方要满足的条件。

必须写的时候:<'a> 在说什么

加上标注就过了:

fn longer<'a>(a: &'a str, b: &'a str) -> &'a str

逐段读:

部分 在说
<'a> 声明一个名字,叫 'a。它是参数,不是值
a: &'a str a 这个引用,至少活过 'a 这段
b: &'a str b 也是
-> &'a str 返回的引用,不会超出 'a

调用时编译器会把 'a 取成 a 和 b 两者里较短的那个,然后检查返回值的 用法没有越过它。这就是上一节那个例子被拒的全部过程。

📌 所以 'a 这个名字本身没有任何含义,它只是个占位符 —— 叫 'x 完全一样。 社区习惯从 'a 开始,仅此而已。

结构体里存引用

一旦结构体里有引用字段,标注就跑不掉了 —— 不写就是 E0106:

struct Excerpt<'a> {
    part: &'a str,
}

它在说:这个 Excerpt 不能比它引用的那个东西活得久。 这件事必须写在类型上,否则任何拿到一个 Excerpt 的人都无从判断它还能不能用。

⚠️ 这也是 <'a> 会「传染」的原因:结构体带了它, impl 块要写 impl<'a> Excerpt<'a>,把它装进别的结构体时那个结构体也得带上。 ⇒ 实践里的判据很简单:能存 String 就别存 &str。 等到确实有性能理由再换,别一上来就为了省一次分配把标注铺满整个类型体系。

'static 不是「永远活着」的开关

'static 是个特殊的名字,意思是「活得和整个程序一样久」。 字符串字面量就是 &'static str, 因为它的内容直接躺在二进制里。

危险的是它看起来像个可以随手加的修饰。 对照项里给一个 String 的引用标上 'static, 报的是 E0597 —— 那个 String 并不会因为你标了 'static 就不被释放。 这正是上面那条「标注不延长任何东西」的又一次体现。

⚠️ 另外有个容易混的地方:&'static T(这个引用指向的东西永远活着) 和 T: 'static(这个类型里不含任何短命的引用)是两件事。 后者在泛型约束里很常见,而一个 String 是满足 T: 'static 的 —— 它自己拥有数据,不借任何人的。

方法为什么更省事

这两段代码的参数个数完全一样, 一个不用标注,一个必须标注 —— 区别只在第一个参数是不是 &self。

这就是省略规则第 3 条:有 &self 时,输出的生命周期直接取它的。 这条规则的现实依据是,方法返回的引用绝大多数时候确实来自 self, 把这个常见情形定成默认,省掉了大量样板。

⇒ 实践推论:如果一个函数需要标注而让你觉得烦,先想想它是不是本来就该是个方法。

下一步

这一篇一直在用 &str 而没解释它 —— 它和 String 到底是什么关系, 为什么函数参数几乎总该写 &str 而不是 &String, 那是切片那一篇的内容。全部篇目见 Rust 教程首页。

本篇示例

下面每一条都是完整的、能单独编译的程序,由npm run test:rust 在每次构建前用真的 rustc 跑一遍。 「编译不过、报 E0382」这种话在这里是被验证过的断言。 报错原文和对照项的结果由同一道闸门自动回写,会随工具链更新,但不作为断言。

大多数时候不用写:一个输入引用的情形

fn first_word(s: &str) -> &str {
    match s.find(' ') {
        Some(i) => &s[..i],
        None => s,
    }
}

fn main() {
    println!("{}", first_word("hello world"));
}

编译通过 · 输出 "hello\n"

对照:改成两个 `&str` 参数(省略规则就推不出来了)
fn longer(a: &str, b: &str) -> &str {
    if a.len() > b.len() { a } else { b }
}

fn main() {
    println!("{}", longer("hello", "hi"));
}

编译不过:error[E0106]

只有一个输入引用时,编译器有唯一的答案可选:输出跟着它。所以你什么都不用写。两个输入引用一进来,唯一性就没了 —— 对照项报的正是 `E0106`。

必须写的时候:把「跟谁一起活」说出来

fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}

fn main() {
    println!("{}", longer("hello", "hi"));
}

编译通过 · 输出 "hello\n"

对照:把三个 `'a` 全部去掉
fn longer(a: &str, b: &str) -> &str {
    if a.len() > b.len() { a } else { b }
}

fn main() {
    println!("{}", longer("hello", "hi"));
}

编译不过:error[E0106]

`<'a>` 不是在声明一个时长,它是在**给一段约束起个名字**:返回的那个引用,不会比 `a` 和 `b` 里先死的那个活得更久。

🚨 标注不延长任何东西

fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}

fn main() {
    let a = String::from("hello");
    let result;
    {
        let b = String::from("hi");
        result = longer(&a, &b);
    }
    println!("{result}");
}

编译不过 · error[E0597]

rustc 原文
error[E0597]: `b` does not live long enough
  --> lifetime-does-not-extend.rs:10:29
   |
 9 |         let b = String::from("hi");
   |             - binding `b` declared here
10 |         result = longer(&a, &b);
   |                             ^^ borrowed value does not live long enough
11 |     }
   |     - `b` dropped here while still borrowed
12 |     println!("{result}");
   |                ------ borrow later used here
对照:把 `b` 的声明提到和 `a` 同一层(去掉那个内层块)
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() > b.len() { a } else { b }
}

fn main() {
    let a = String::from("hello");
    let b = String::from("hi");
    let result = longer(&a, &b);
    println!("{result}");
}

编译通过,输出:"hello\n"

这是全篇最该记住的一条:函数签名一字未改、标注一个不少,照样编译不过。`b` 在内层块结束时就没了,而 `result` 要活到 `println!` —— 标注只是**陈述**了约束,它不会去改变任何一个值的实际寿命。

结构体里存引用,就得带上标注

struct Excerpt {
    part: &str,
}

fn main() {
    let s = String::from("hello world");
    let e = Excerpt { part: &s[..5] };
    println!("{}", e.part);
}

编译不过 · error[E0106]

rustc 原文
error[E0106]: missing lifetime specifier
 --> lifetime-struct-field.rs:2:11
  |
2 |     part: &str,
  |           ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct Excerpt<'a> {
2 ~     part: &'a str,
  |
对照:给结构体加 `<'a>`,字段写成 `&'a str`
struct Excerpt<'a> {
    part: &'a str,
}

fn main() {
    let s = String::from("hello world");
    let e = Excerpt { part: &s[..5] };
    println!("{}", e.part);
}

编译通过,输出:"hello\n"

结构体存引用意味着「这个结构体不能比它引用的那个东西活得久」,而这件事必须写在类型上 —— 否则任何拿到 `Excerpt` 的人都无从判断它还能不能用。这就是 `<'a>` 会在结构体上「传染」的原因。

`'static`:整个程序都活着的那一类

fn main() {
    let s: &'static str = "我被编译进了二进制";
    println!("{s}");
}

编译通过 · 输出 "我被编译进了二进制\n"

对照:换成一个 `String` 的引用(它在运行时才分配)
fn main() {
    let owned = String::from("我在运行时才分配");
    let s: &'static str = &owned;
    println!("{s}");
}

编译不过:error[E0597]

字符串字面量是 `&'static str`,因为它的内容直接躺在二进制里,程序活多久它活多久。⚠️ 但 `'static` **不是「让它永远活着」的开关** —— 对照项里那个 `String` 并不会因为你标了 `'static` 就不被释放。

方法比函数省事:`&self` 优先

struct Parser {
    text: String,
}

impl Parser {
    fn head(&self, _other: &str) -> &str {
        &self.text[..2]
    }
}

fn main() {
    let p = Parser { text: String::from("hello") };
    println!("{}", p.head("忽略我"));
}

编译通过 · 输出 "he\n"

对照:把 `&self` 换成一个普通的 `&str` 参数(变成关联函数)
struct Parser {
    text: String,
}

impl Parser {
    fn head(text: &str, _other: &str) -> &str {
        &text[..2]
    }
}

fn main() {
    println!("{}", Parser::head("hello", "忽略我"));
}

编译不过:error[E0106]

两个版本的参数个数完全一样,一个不用标注一个必须标注 —— 区别只在第一个参数是不是 `&self`。省略规则的第三条就是专门为方法写的:有 `&self` 时,输出的生命周期直接取它的。