一句话总结
生命周期(Lifetime)不是一种新机制, 而是给脑中"这个引用不能比它指向的数据活得久"这条常识起个名字, 写进类型签名.
编译器一直在做这种检查; 生命周期标注只是在它无法自行推断时, 由程序员来挑明引用之间的存活关系.
理解了这层, &'a 就不再神秘.
从一个无法回答的问题说起
先看一个会报错的函数--返回两个字符串切片中较长的那个:
|
函数体逻辑完全正确, 为什么编译器拦下它? 因为编译器在为调用方担心一件事: 返回的那个引用, 到底借的是 x 的数据还是 y 的数据? 它的存活范围该按谁算?
编译器无法从签名推断出来--它不知道运行时会走 if 还是 else. 而这个信息至关重要, 因为:
|
问题的本质
编译器必须保证: longest 返回的引用, 不会比它借用的任何输入数据活得更久. 但它没法靠分析函数体得出结论(分支在运行时才确定).
于是它要求函数的作者--用生命周期标注把这层关系明说出来.
生命周期到底是什么
先建立最核心的直觉. 每个引用都有一个"生命周期": 它有效的那段代码范围. 这件事一直存在--第 01 篇里 NLL 计算的"借用范围", 就是生命周期. 多数时候编译器自动推断, 根本不用写.
生命周期标注('a 这种带单引号的名字)登场, 只在一种情况: 当一个函数/类型涉及多个引用, 而它们的存活关系会影响安全, 编译器又推断不出时, 需要用标注描述这些引用之间的关系.
生命周期标注像合同里的"有效期联动条款": 它不改变任何东西的实际寿命, 只是声明"这几份引用的有效期必须挂钩".
'a 不是"让 x 活 5 秒"的命令, 而是"返回值的有效期 = x 和 y 中较短的那个"的约定. 编译器照着这份约定去检查调用方有没有违约.
最常见的误解, 先破除
'a 不会延长或缩短任何数据的实际存活时间. 它纯粹是描述性的--告诉编译器引用之间已经存在的关系, 好让它据此检查.
写 'a 不会让数据多活一秒. 把它理解成"命令"是绝大多数初学者的卡点.
给 longest 加上标注
现在修复 longest. 要表达的关系是: "返回的引用, 和 x, y 一样长寿(不会超过它们中较短者)":
|
逐部分拆解这个签名:
<'a>: 声明一个生命周期参数'a(像泛型类型参数<T>那样, 先声明).x: &'a str,y: &'a str: 两个入参的生命周期都标为'a.-> &'a str: 返回值的生命周期也是'a.
合起来读: "存在某个生命周期 'a, x, y 至少活这么久, 而返回值也只活这么久." 编译器据此推断: 'a 取 x 和 y 中较短的那个寿命, 返回值不能超过它. 于是开头那段悬垂引用的代码就会被正确地拒绝.
把 'a 看成"取最小值"的占位符: 调用时, 编译器把 'a 实例化为所有标了 'a 的输入引用里最短的那个寿命.
返回值被绑到 'a, 自然不可能比任何一个输入活得久--悬垂引用从根上被排除.
不是所有引用都要标同一个生命周期
标注是用来表达真实关系的, 不要无脑全标 'a. 如果返回值只可能来自 x, 那就只有 x 需要和返回值挂钩, y 可以是任意独立的生命周期:
|
思想: 标注是在'如实陈述', 不是'满足编译器'
正确的心态是: 先想清楚"这个函数返回的引用, 实际借的是哪个输入的数据", 再把这层关系用 'a 如实写出来.
标注错了(比如把不相关的 y 也绑上), 要么编译不过, 要么让调用方受到不必要的约束. 生命周期标注是设计的一部分, 而非编译器的刁难.
快速回顾
- 本质: 生命周期是"引用有效的代码范围", 一直存在; 标注只在编译器推断不出引用关系时才需要.
- 触发场景: 函数涉及多个引用, 返回引用, 且其来源影响安全时, 编译器要求标注.
- 纯描述性:
'a不改变任何数据的实际寿命, 只声明引用之间的存活关系(破除最大误解). - 读法:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str= "返回值不超过 x, y 中较短者的寿命". - 如实标注: 按引用的真实来源关系标, 不要无脑全标同一个
'a.
动手练习
-
不看下文, 凭直觉判断: 下面的函数需要生命周期标注吗? 为什么?
fn longest(x: &str, y: &str) -> &str { ... }
参考答案
需要. 它返回一个引用, 而这个引用可能来自 x 也可能来自 y, 编译器无法推断返回引用的存活范围该按谁算, 因此要求用 'a 标注挑明关系:
|
-
下面的代码为什么报错? 用"生命周期是描述性的"这一点解释.
let r;
{
let s = String::from("hi");
r = &s;
}
println!("{r}");
参考答案
s 在内层作用域结束时被释放, 而 r 借用了 s. r 的生命周期(用到 println!)超过了 s 的生命周期, 形成悬垂引用. 编译器报 "borrowed value does not live long enough".
这里没有任何标注, 编译器靠自动推断就发现了"引用比数据活得久"--生命周期检查一直在进行, 标注只是它在更复杂场景下的"辅助说明".
-
给下面的函数加正确的生命周期标注. 它总是返回第一个参数.
fn pick(a: &str, b: &str) -> &str { a }
参考答案
|
返回值只可能来自 a, 所以只把 a 和返回值绑定到 'a; b 与返回值无关, 不必标 'a(让它保持独立的匿名生命周期). 如实陈述"返回值借的是 a"即可.
- 思考题: 把
longest的返回值改成"总是返回一个函数内部新建的String的引用", 可行吗?
参考答案
不可行. 函数内新建的 String 在函数返回时就被释放, 返回它的引用必然悬垂--无论怎么标生命周期都救不了(因为这个引用的数据根本活不过函数边界).
正确做法是直接返回 String(移交所有权), 而不是返回引用. 这也再次印证: 标注不能延长寿命, 数据本身活不够久, 标注无能为力.