生命周期标注为引用的有效区间命名,用来表达输入引用、输出引用和借用字段之间的关系; 它是类型契约,不会让任何值活得更久。
给多个引用写同一个 'a 会建立约束,却不代表它们来自相同词法作用域;
滥加 'static 也不能修复局部引用。
先追踪每个返回引用来自哪个输入,只标注真正的依赖; 能由省略规则唯一确定时就省略,数据必须独立存在时则返回拥有型值。
是什么,为什么存在
生命周期(lifetime) 是引用可以被安全使用的程序区域。 每条引用都有生命周期,但大多数生命周期由编译器推断,不需要写在源码中。 它描述的是引用的有效性,不等同于变量可见的整个词法作用域。
生命周期标注(lifetime annotation) 以撇号开头,例如 'a、'input 和 'static。
它像类型参数一样在泛型参数列表中声明,但代表区域关系,而不是一种运行时类型。
标注只出现在类型与签名中,编译后不需要保存一份生命周期值。
当函数接收多个引用并返回引用时,函数体可能从不止一个输入借出数据。
仅凭 fn select(x: &str, y: &str) -> &str,调用方看不出结果依赖 x、y 还是两者。
显式参数把这种来源关系变成可检查的 API 契约。
例如,fn select<'a>(x: &'a str, y: &'a str) -> &'a str 表示调用方要选择一个区域 'a,两个输入在该区域内都有效,结果也只能在该区域内使用。
它不要求两个所有者在同一个代码块创建或销毁;较长的借用可以缩短到共同可用的区域。
函数实现必须对每一种满足契约的调用都安全。
借用检查器(borrow checker) 结合签名、控制流与引用的实际使用位置求解这些约束。 若结果可能越过来源数据的有效区间,它会在编译时拒绝代码。 生命周期标注给分析增加关系,却不会改变所有者的销毁位置。
你会在返回切片的函数、保存引用的结构体、闭包和 trait 对象、线程边界以及泛型 trait 约束中遇到这套语法。 普通只读参数往往可以依赖省略规则;跨越函数边界保存或返回借用时,关系才更常需要显式表达。
工作原理
签名是一组约束
生命周期参数在函数名后的尖括号中声明,再写到 & 与被引用类型之间。
&'a T 是生命周期为 'a 的共享引用,&'a mut T 是同一区域内的可变引用。
描述性名称如 'source 适合复杂签名,简短关系通常使用 'a 和 'b。
输出上的 'a 必须与某个有效来源建立联系。
编译器不会根据函数返回时碰巧执行的分支,为公开签名推导条件式关系。
如果实现可能返回两个输入之一,就要给两个输入与输出提供共同约束。
同名参数表达的是最低保证,而不是强制实际借用拥有完全相同的长度。
在调用处,编译器可以把较长引用重新借用为更短引用,并选择满足全部位置的区域。
因此,“同一个 'a”应读成“在同一个所选区域内有效”。
不同名称表示签名没有自动建立关系。
若 fn prefix<'text>(text: &'text str, delimiter: &str) -> &'text str 只从 text 返回切片,分隔符的借用就不应限制结果。
精确签名既扩大合法调用范围,也让审查者看清数据来源。
省略是确定规则
生命周期省略(lifetime elision) 不是任意猜测。 编译器按固定规则补全函数、函数指针和闭包 trait 签名;规则结束后仍有歧义,就会报告缺少生命周期说明符。
函数省略规则依次是:
- 每个省略的引用参数获得一个互不相同的输入生命周期。
- 若全部参数中恰好只有一个输入生命周期,所有省略的输出生命周期都使用它。
- 方法有多个输入生命周期时,只要接收者是
&self或&mut self,所有省略的输出生命周期都使用接收者的生命周期。
因此,fn first(text: &str) -> &str 可以省略,因为只有一个输入生命周期。
fn choose(x: &str, y: &str) -> &str 不能省略,因为规则无法判断输出来自哪个输入。
方法的接收者规则还可能让输出默认绑定到 self,即使另一个参数看起来更像来源。
占位生命周期 '_ 要求编译器在当前位置推断生命周期。
它在类型路径中比完全省略更明确,例如返回 View<'_>;但它仍不会创建新的关系或放松借用检查。
借用字段把关系带进类型
结构体若保存引用,就必须在结构体定义上声明相应生命周期。
struct View<'a> { text: &'a str } 表示任何 View<'a> 都不能在 text 已失效后继续使用。
约束跟随这个类型进入函数参数、返回值与容器元素。
impl<'a> View<'a> 声明实现适用于任意 'a。
方法返回字段时,可以明确写 -> &'a str,表示结果直接借自字段的来源;写 -> &str 时,接收者省略规则通常把结果限制到这次 &self 借用。
两种签名可能都能编译,但对调用方暴露的关系不同。
借用字段适合短期视图、解析结果和避免复制的大块输入。
如果值要进入长期缓存、跨线程队列或独立于来源存储,拥有 String、Vec<T> 或共享所有权指针通常更符合契约。
生命周期参数不能让自引用结构体自动变得安全,也不能替代所有权设计。
先决定所有权,再写标注
返回引用适合结果本来就是输入的一部分,而且调用方能自然保留输入的场景。 切片查找、无复制解析和集合视图都符合这种关系;签名让调用方知道结果不能脱离来源。
返回拥有型值适合函数创建新数据,或结果必须独立进入队列、缓存与异步任务的场景。 这不是对借用检查器的妥协,而是不同的 API 契约;强行借用反而会把内部存储细节传播给所有调用方。
结构体是否保存引用也应由对象的角色决定。 一次请求内使用的解析视图可以借用缓冲区,而长期领域对象通常应拥有关键字段。 先画出所有者与使用期限,再决定是否需要结构体生命周期参数。
编译器建议添加 'a 时,只说明签名缺少可证明的关系,不保证标注是正确修复。
若数据在函数内创建,返回引用仍然没有合法来源;若输出只来自一个输入,把其他参数绑进同一 'a 又会过度约束。
最小契约通常也是最稳定的契约。 它减少调用方必须同时维持的借用,让函数实现的真实数据流直接体现在类型中。 这也让后续重构更容易区分必要约束与偶然耦合。
示例
为两个输入建立共同区域
第一个例子可能返回主标签,也可能返回备用标签,所以三个引用位置使用同一个 'a。
两个 String 的词法作用域不同,但调用只在内部代码块使用结果,编译器可以选择这段共同区域。
fn choose_label<'a>(primary: &'a str, fallback: &'a str) -> &'a str {
if primary.trim().is_empty() {
fallback
} else {
primary
}
}
fn main() {
let primary = String::from("priority");
{
let fallback = String::from("untitled");
let selected = choose_label(&primary, &fallback);
println!("selected: {selected}");
}
}selected: priorityselected 的可用区间不会超过两个输入借用的交集。
即使运行时选中了 primary,签名仍允许函数返回 fallback,所以调用方不能把结果带出内部代码块。
标注没有把 primary 缩短,也没有延长 fallback。
它只要求结果的每次使用都位于两者共同有效的区域内。
只关联真正的来源
第二个例子的结果只能来自 text,不会来自 delimiter。
签名只为文本与结果命名 'text,分隔符使用独立的省略生命周期。
fn prefix_before<'text>(text: &'text str, delimiter: &str) -> &'text str {
text.split_once(delimiter)
.map_or(text, |(prefix, _)| prefix)
}
fn main() {
let record = String::from("account=active");
let key = {
let delimiter = String::from("=");
prefix_before(&record, &delimiter)
};
println!("key: {key}");
}key: accountdelimiter 在内部代码块结束时销毁,key 仍然有效,因为它只指向 record。
若把两个参数都标成 'text,返回值会被分隔符的短借用不必要地限制。
这种精确度对解析器和查找 API 很重要。 辅助输入只参与计算时,不应被误写成返回数据的来源。
在结构体中保存借用
Header<'source> 保存原始文本的视图,而不复制字符串。
name 的输出按方法省略规则绑定到 &self,raw 则明确返回来源生命周期 'source。
struct Header<'source> {
raw: &'source str,
}
impl<'source> Header<'source> {
fn new(raw: &'source str) -> Self {
Self { raw }
}
fn name(&self) -> &str {
self.raw
.split_once(':')
.map_or(self.raw, |(name, _)| name)
}
fn raw(&self) -> &'source str {
self.raw
}
}
fn main() {
let source = String::from("content-type:text/plain");
let header = Header::new(&source);
println!("name: {}", header.name());
let complete = header.raw();
drop(header);
println!("raw: {complete}");
}name: content-type
raw: content-type:text/plaincomplete 借自 source,不是借自结构体本身,所以移动并销毁 header 后仍能读取它。
若方法写成省略的 -> &str,返回引用通常只能保证持续到 &self 借用结束。
结构体的生命周期参数没有拥有文本。
source 仍必须比所有从它派生的视图活得更久。
要求回调接受任意短借用
最后一个例子把规范化函数应用到多个临时字符串切片。
for<'a> 要求回调对每个调用选择的 'a 都成立,并把结果关联回该次输入。
fn normalize_all<F>(values: &[String], normalize: F) -> Vec<&str>
where
F: for<'a> Fn(&'a str) -> &'a str,
{
values.iter().map(|value| normalize(value)).collect()
}
fn trim_label(value: &str) -> &str {
value.trim()
}
fn main() {
let labels = vec![String::from(" alpha "), String::from(" beta")];
let normalized = normalize_all(&labels, trim_label);
println!("normalized: {normalized:?}");
}normalized: ["alpha", "beta"]返回向量中的每个 &str 都借自 labels 中对应的 String。
回调不能返回某个与输入无关、寿命更短的局部切片。
普通函数 trim_label 满足高阶约束,因为它能处理任意有效输入借用。
这种边界常见于保存泛型闭包并在方法内部借用自身数据的 API。
陷阱
省略、边界与高阶约束
长于关系
长于约束(outlives bound) 'long: 'short 表示 'long 至少覆盖 'short。
若函数要在要求 'short 的位置返回一个 'long 引用,这个约束说明较长引用可以缩短后使用。
边界描述可替换关系,不会延长底层值。
类型边界 T: 'a 表示 T 中包含的所有引用都至少对 'a 有效。
对 &'a T 来说,类型良构本身已经隐含 T: 'a,所以现代 Rust 中许多旧式显式边界是冗余的。
trait 边界不会以同样方式全部自动推导,不能把两类规则混为一谈。
长于关系通常出现在组合多个借用字段、泛型关联类型或将较长借用交给较短接口时。
若签名只处理一个普通 &T,先检查省略和隐含边界是否已经足够。
'static 的两种读法
静态生命周期(static lifetime) 覆盖程序的整个执行期。
字符串字面量和 static 项可以产生 &'static T,因为其引用目标在程序结束前不会销毁。
这描述引用目标的有效期。
T: 'static 是类型边界,表示 T 不含短于静态期的借用。
拥有型 String 满足这个边界,却仍可在普通代码块末尾被立即销毁;边界没有承诺值本身永远存在。
相反,指向局部 String 的 &str 通常不满足需要 'static 的线程或回调边界。
trait 对象还有默认对象生命周期规则。
在没有外层约束的类型位置,Box<dyn Trait> 通常等价于 Box<dyn Trait + 'static>;需要保存非静态借用时应明确写出合适的 + 'a 或 + '_。
这套默认规则不同于普通函数的三条省略规则。
对所有生命周期成立
高阶 trait 约束(higher-ranked trait bound,HRTB) 把生命周期参数放在 for<'a> 中。
F: for<'a> Fn(&'a str) -> &'a str 表示 F 必须接受任意调用时才确定的输入借用,并返回与该次输入关联的借用。
普通 F: Fn(&'a str) -> &'a str 中的 'a 往往由外层调用方选择一次。
高阶约束则允许被调用函数在每次内部借用时选择新的短生命周期,这正是对局部数据反复调用回调时需要的能力。
函数指针与闭包 trait 的常见省略形式会自动得到类似高阶含义,例如 fn(&str) -> &str 可展开为 for<'a> fn(&'a str) -> &'a str。
显式写出 for<'a> 适合复杂泛型边界,也能让审查者看出量化发生在哪里。
从诊断反推契约
E0106 通常表示输出引用的来源仍有歧义,先检查签名缺少哪条输入输出关系。
E0515 常指向返回了局部数据的引用,这种错误不能靠增加任意生命周期参数修好。
E0597 则表示某次实际调用让被借用值过早销毁,需要调整使用区间或所有权。
修复顺序应从数据来源开始:确认输出指向谁,再确定调用方需要使用多久,最后写最小约束。 若没有任何输入或字段能拥有返回数据,就改为返回拥有型值。 只有契约本身正确后,缩短借用区间、重借用或拆分结构才有意义。
不要用 unsafe 把编译错误变成无法检查的承诺。
生命周期诊断往往暴露了真实的所有权缺口;绕开检查只会把失败从编译期移动到未定义行为。
延伸阅读
4个问题 · 1 道输出预测题 · 1 道找错题