所有权:编译器为什么拒绝我
生命周期标注到底在说什么
它在解决什么
借用那篇最后撞上了一个新问题:想从函数里返回一个引用,
编译器报的是 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> 是很正常的 —— 不是因为它不重要,
而是因为编译器在大多数情况下能自己推出唯一答案。这套推导叫省略规则,
只有三条:
- 每个被省略的输入引用,各自分配一个独立的生命周期。
- 如果只有一个输入生命周期,就把它赋给所有被省略的输出。
- 如果有多个输入,但其中一个是
&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` 时,输出的生命周期直接取它的。