生命周期(lifetime) 是引用保持有效的程序区域;编译器借此证明引用的每次使用都早于来源失效。
'a 不会延长数据寿命,同名生命周期也不要求所有者在同一词法作用域中创建和销毁。
先追踪每条返回引用的数据来源,再用最小约束表达关系;数据必须独立存在时,返回拥有型值。
是什么,为什么存在
Rust 的每条引用都有生命周期,也就是该引用可以安全使用的一段程序区域。 借用检查器(borrow checker) 确认引用在每次使用时仍指向有效数据,并确认共享访问与独占访问不会发生非法重叠。这项分析在编译期完成,运行时没有生命周期计时器。
生命周期解决的是悬垂引用问题。若一条引用在所有者被销毁后仍可使用,读取它就可能访问已经释放或改作他用的内存。Rust 不允许程序把这种风险留到运行时,因此无法证明来源仍有效的借用会导致编译失败。
源码中的大多数生命周期是隐式的。编译器根据所有者、引用的创建位置、控制流和引用的实际使用位置推导有效区域; 非词法生命周期(non-lexical lifetimes,NLL) 意味着借用通常可以在最后一次使用后结束,而不必一直持续到右花括号。
显式 生命周期标注(lifetime annotation) 以撇号开头,例如 'a 或 'input。它给多个引用之间的关系命名,使函数调用方和实现都受同一类型契约约束。标注描述已有关系,不会改变值何时销毁,也不会分配或复制数据。
返回输入切片的函数、保存借用字段的结构体、trait 对象、闭包以及跨线程任务都会暴露生命周期边界。只在调用期间读取参数时,&T 往往已经足够;当引用要被返回、存储或隐藏在泛型接口之后时,关系才更常需要写出来。
生命周期首先是所有权设计问题,其次才是语法问题。若函数创建新数据,返回 String 或其他拥有型值通常最自然;若结果确实是输入的一部分,返回引用可以避免复制,并在类型中保留来源关系。
工作原理
区域来自实际使用
编译器为每次借用寻找一个满足全部约束的有效区域。这个区域必须覆盖引用的每次使用,却不能越过来源数据的有效期,也不能与冲突访问重叠。区域经常比变量所在的词法代码块短。
下面的流程图展示一次普通共享借用何时可以结束。最后一次使用之后若不再读取该引用,后续代码就可以开始需要独占访问的操作。
借用结束不等于所有者被销毁。所有者可以继续存在并被再次借用,原引用变量甚至仍在词法作用域内,只要之后不再使用它即可。诊断冲突时,应找出引用创建点、冲突访问和让旧借用继续有效的最后一次使用。
标注约束输入与输出
签名 fn choose<'a>(x: &'a str, y: &'a str) -> &'a str 声明一个生命周期参数,并把两个输入和输出放进同一关系。调用方需要提供一个区域,使两个输入借用在其中都有效;返回引用也只能在这个共同区域内使用。
同名 'a 不表示两个所有者必须活得一样久。编译器可以把较长借用缩短到共同可用的区域。因为函数实现可以返回任一输入,调用方必须保守地让两个输入都覆盖结果的使用范围。
若函数只可能返回第一个输入,第二个参数就不该限制结果。fn prefix<'text>(text: &'text str, delimiter: &str) -> &'text str 精确说明结果来自 text,而 delimiter 只在调用期间使用。独立关系能接受更多合法调用,也能让审查者看出数据来源。
生命周期参数由调用方在每次调用处选择,函数实现必须对所有合法选择都成立。它不能把局部 String 的切片伪装成任意 'a 返回,因为局部所有者在函数结束时就会销毁,无法满足调用方可能要求的区域。
省略是确定规则
生命周期省略(lifetime elision) 是一组固定的签名补全规则,不是根据函数体猜测。每个省略的引用参数先获得独立的输入生命周期;若只有一个输入生命周期,省略的输出生命周期就使用它;方法有多个输入生命周期时,&self 或 &mut self 的生命周期会赋给省略的输出。
因此,fn first(text: &str) -> &str 可以省略标注,而 fn choose(x: &str, y: &str) -> &str 无法通过省略决定输出来源。方法 fn name(&self, fallback: &str) -> &str 的输出默认借自 self,并不会因为实现返回 fallback 而自动改变。
占位生命周期 '_ 要求编译器在当前位置推断,例如 View<'_>。它适合在类型路径中明确表示这里存在生命周期参数,但不会新建关系或绕过检查。能省略时通常无需为了显得明确而重复写出 'a。
借用字段把约束带进类型
结构体保存引用时,类型本身必须携带生命周期,例如 struct View<'a> { text: &'a str }。这表示任何 View<'a> 的使用都不能越过 text 的有效区域。约束会跟随该类型进入返回值、容器和其他结构体字段。
impl<'a> View<'a> 表示实现适用于任意 'a。方法返回 -> &'a str 时,结果可以直接继承字段来源的生命周期;写成 -> &str 时,接收者省略规则通常把结果限制到本次 &self 借用。两种签名可能都能编译,但公开契约并不相同。
多个独立借用字段不必共享一个参数。把来源数据与临时缓冲区都写成 'a,会让整个结构体受较短输入限制;分别使用 'source 与 'buffer,或直接拥有临时缓冲区,通常更精确。
'static 有两种常见位置
静态生命周期(static lifetime) 写作 'static。&'static str 表示引用目标在程序整个执行期间都有效,字符串字面量和 static 项是典型来源。局部 String 的引用不会因为加上标注而变成静态引用。
类型约束 T: 'static 的含义不同:T 不能包含短于程序执行期的借用。拥有型 String 满足这项约束,但它仍可在普通作用域末尾销毁;约束说明它可以被安全保留多久,而不是实际会保留多久。
线程与长期保存的回调常要求 T: 'static,因为调用方不能保证任务何时结束。正确做法通常是移动拥有型数据或适当的共享所有权指针。move 闭包若捕获的值本身是短期引用,仍然不满足 'static。
示例
下面四个示例依次展示最后一次使用、共同区域、精确来源和高阶约束。每个输出都由本地 Rust 编译器实际编译并运行对应文件得到。
最后一次使用结束借用
first 的最后一次使用发生在 push() 之前,因此共享借用可以先结束。变量仍处于同一代码块,不会阻止之后对向量进行可变借用。
fn main() {
let mut latencies = vec![18, 21, 16];
let first = &latencies[0];
println!("first: {first}");
// first 不再使用,共享借用可以在这里结束。
latencies.push(19);
println!("all: {latencies:?}");
}first: 18
all: [18, 21, 16, 19]若把 println!("first: {first}") 移到 push() 后面,代码就会被拒绝。push() 可能重新分配向量缓冲区,而后续使用要求旧元素引用继续有效;真正决定冲突的是最后一次使用,不是引用声明到右花括号的距离。
为两个来源选择共同区域
choose() 可以返回任一输入,所以三个位置共享 'a。内部字符串只在内部代码块中有效,结果也只在该块内使用。
fn choose<'a>(primary: &'a str, fallback: &'a str, healthy: bool) -> &'a str {
if healthy {
primary
} else {
fallback
}
}
fn main() {
let stable = String::from("stable");
{
let temporary = String::from("temporary");
let selected = choose(&stable, &temporary, true);
println!("selected: {selected}");
}
println!("owner remains: {stable}");
}selected: stable
owner remains: stable运行时虽然选择了 stable,公开签名仍允许实现返回 temporary。编译器不会根据布尔参数的值为结果建立条件式生命周期,因此调用方必须在两个输入都有效的区域内使用 selected。
分开数据来源与辅助参数
parse_field() 返回借自 record 的结构体,而 delimiter 只参与查找。分隔符在内部代码块末尾销毁后,结果仍可使用,因为返回类型没有与它绑定。
struct Field<'a> {
name: &'a str,
value: &'a str,
}
fn parse_field<'record>(record: &'record str, delimiter: &str) -> Option<Field<'record>> {
let (name, value) = record.split_once(delimiter)?;
Some(Field { name, value })
}
fn main() {
let record = String::from("region=eu-west");
let field;
{
let delimiter = String::from("=");
field = parse_field(&record, &delimiter).expect("valid field");
}
println!("{} -> {}", field.name, field.value);
}region -> eu-west若把 delimiter 也标成 'record,调用方会被迫让临时分隔符存活到 field 的最后一次使用。那不是安全所需的约束,而是过度约束;精确的生命周期让辅助输入可以更早结束。
要求函数适用于任意借用
print_views() 在每次循环中借用不同的 String。约束 for<'a> 要求 view 对每个合适的输入生命周期都保持同一输入输出关系。
fn first_word(input: &str) -> &str {
input.split_whitespace().next().unwrap_or("")
}
fn print_views<F>(records: &[String], view: F)
where
F: for<'a> Fn(&'a str) -> &'a str,
{
for record in records {
println!("{}", view(record));
}
}
fn main() {
let records = vec![
String::from("alpha ready"),
String::from("beta waiting"),
];
print_views(&records, first_word);
}alpha
beta普通函数参数可以通过省略得到这种输入输出关系,但泛型约束需要表达选择权属于每次内部调用。高阶约束不是让引用变长,而是要求可调用对象不能只适用于某一个由外部预先固定的生命周期。
陷阱
把标注当作延寿操作
修复方法: 函数创建数据时返回拥有型值;返回引用时,把来源追溯到输入、结构体字段、字符串字面量或 static 项。不要用更多标注掩盖没有合法来源的返回值。
用同一个名称绑定独立输入
修复方法: 为每条可能返回的路径标出来源。辅助引用使用独立生命周期或省略;只有实现可能返回多个输入之一时,才把这些输入与输出放进共同关系。
把 move 等同于 'static
修复方法: 检查每个捕获值内部是否含借用。任务必须独立时,移动 String、Vec<T>、Arc<T> 或其他符合所有权边界的值,并把共享修改和线程安全作为另一个问题审查。
用额外代码块猜借用终点
修复方法: 从编译器诊断中的借用创建点、冲突点和后续使用点读取时间线。删除或提前完成最后一次使用,或者重新组织所有权;只在代码块确实表达资源或变量边界时增加它。
用泄漏绕过 'static 错误
修复方法: 优先把拥有型数据移入任务,或用 Arc 表达共享所有权。只有数据按设计应在进程余下时间都保留,而且增长有明确上限时,才考虑有意泄漏。
超越关系与重新借用
超越生命周期约束(outlives bound) 写作 'long: 'short,表示 'long 至少与 'short 一样长。它允许编译器在需要较短借用的位置使用较长来源,因为较长引用可以重新借用成覆盖更小区域的引用。
重新借用不会复制目标数据。对 &T 重新借用会创建一条更短的共享引用;对 &mut T 重新借用时,原可变引用在新借用使用期间不能被同时使用,结束后才可恢复。很多看似自动的生命周期缩短,本质上都依赖这种关系。
约束方向容易读反。'a: 'b 应读成 'a 超越 'b,而不是 'a 被 'b 包含;画时间区间时,'a 覆盖的区域至少包含 'b。只在泛型接口无法从其他类型关系推导时显式写出它。
型变决定哪些缩短安全
对生命周期而言,&'long T 通常可以用在需要 &'short T 的位置,只要 'long: 'short。这种协变让只读引用自然缩短,也让 Vec<&'static str> 可以在适当位置作为包含较短引用的值使用。
可变引用对自身生命周期仍是协变的,但对被引用类型 T 是不变的。若允许把 &mut &'static str 当作 &mut &'short str,调用方就可能写入一条短期引用,随后原位置却仍被当作静态引用读取。禁止这种替换才能保护写入后的类型承诺。
函数参数位置还会反转子类型方向,因此涉及函数指针、闭包和嵌套引用时,直觉很容易失效。不要靠反复添加生命周期碰运气;先确定每个类型构造器是只读、可写还是可调用,再检查它在该参数上的型变。
| 类型位置 | 常见型变 | 审查重点 |
|---|---|---|
&'a T 对 'a | 协变 | 较长借用可以缩短 |
&'a mut T 对 'a | 协变 | 新借用期间保持独占 |
&'a mut T 对 T | 不变 | 防止写入更短引用 |
fn(T) 对 T | 逆变 | 接受范围必须足够宽 |
高阶约束把选择推迟到调用处
高阶 trait 约束(higher-ranked trait bound,HRTB) 使用 for<'a> 表达对每个合适生命周期都成立。F: for<'a> Fn(&'a str) -> &'a str 表示内部每次调用都可选择新的 'a,并要求返回值始终借自那次输入。
普通 F: Fn(&'env str) -> &'env str 则由外层接口选择 'env。若实现要把自己的局部临时借给 F,这个预先固定的区域可能过长,无法由局部值满足。高阶约束把量词放到正确一层,而不是延长局部值。
函数指针与 Fn trait 的常见省略形式经常隐含高阶关系,所以简单签名无需总写 for<'a>。当回调被存进泛型结构、返回借用或组合多层 trait 约束时,显式写法更容易审查。
trait 对象的默认生命周期边界
trait 对象有自己的对象生命周期边界,例如 dyn Display + 'a。它限制对象内部可能包含的借用,不是对保存该对象的 Box 或引用重复标注。省略对象边界时会应用一套默认规则,某些拥有型位置最终默认为 'static。
因此,Box<dyn Trait> 可能拒绝包含短期借用的具体类型,即使盒子本身只在一个短代码块内使用。需要保存借用对象时,应在 API 上写出 Box<dyn Trait + 'a> 并让容器携带 'a;需要真正独立对象时,则让具体值拥有所需数据。
引用形式 &'a dyn Trait 往往能从包含它的引用推断对象边界,但嵌套别名和返回类型会让默认规则变得不明显。公开 API 中若边界影响可接受的实现,应显式表达,避免调用方把默认 'static 当成编译器的任意要求。
析构与自引用边界
带借用字段的类型在销毁时也必须保持所借数据有效。泛型类型若实现 Drop,编译器会保守检查析构代码可能访问哪些参数;不能假设字段销毁顺序或手写标注会允许析构读取已失效数据。
普通结构体不能安全地同时拥有一个值和指向该值内部的引用,因为移动结构体可能改变所拥有值的地址,而生命周期参数描述外部来源,不能表达指向自身字段。索引、偏移或拥有型解析结果常是更简单的设计。
确实需要地址稳定的自引用抽象时,Pin 只解决不再移动这一部分,还需要正确初始化顺序、投影规则和析构不变量。除非现有安全抽象无法满足需求,不应让生成代码直接组合原始指针、Pin 与伪造生命周期。
方法同时涉及两层借用
impl<'source> View<'source> 上的 'source 来自结构体字段,而方法调用还会为 &self 创建一次通常更短的借用。返回 &'source str 表示结果直接继承字段来源;返回 &str 则通常把结果关联到方法接收者的本次借用。
更长的返回生命周期不一定更好。若方法通过内部可变性更新缓存,返回值受 &self 的本次借用限制,可能正是维护不变量所需的契约。公开 'source 会允许结果在接收者借用结束后继续存在,只有字段来源确实支持时才应这样写。
方法还可以声明自己的生命周期参数,它与 impl 上的参数不是同一层。描述性名称有助于区分存储来源、接收者借用和临时参数,避免一串同名 'a 隐藏真实关系。
| 关系 | 典型含义 | 设计问题 |
|---|---|---|
| 字段来源到结果 | 结果可以脱离本次接收者借用 | 字段是否直接保存该来源 |
&self 到结果 | 结果只随本次方法借用有效 | 方法是否返回内部视图 |
| 临时参数到结果 | 结果可能借自调用参数 | 实现是否真的返回该参数 |
闭包捕获也有来源边界
闭包的捕获生命周期通常由编译器推断,但它们仍进入闭包的匿名类型。借用局部变量的闭包不能被保存到超过局部变量的容器中;move 只决定捕获值如何进入闭包,不会删除值内部的引用。
接受并立即调用闭包的函数通常可以容纳短期捕获。把闭包返回、装箱为 trait 对象、注册到长期回调或发送到线程时,保存边界会暴露更多约束。此时应从闭包字段的角度审查每个捕获值由谁拥有。
自动给回调添加 'static 会缩小 API 可接受的闭包集合。只有实现确实可能在当前调用结束后保留闭包时才需要这项边界;若函数保证在返回前完成调用,较短借用通常应被允许。
临时值延长规则很窄
某些 let 绑定会把临时值的销毁推迟到包围作用域末尾,例如把借用表达式直接绑定到局部引用。这是针对特定语法形状的临时生命周期延长,不是通用的借用延长机制,也不是生命周期标注的效果。
函数实参中的临时值通常只持续到包含调用的语句末尾。若返回值可能借自该临时值,就不能在后续语句中继续使用结果。把临时值先绑定到拥有型局部变量,可以明确它应活到哪里。
不要把一个能编译的临时借用形状类推到重构后的表达式。增加辅助函数、改变模式或把表达式移入另一个语法位置,都可能改变临时值的销毁点;所有权边界应由有名称的拥有型绑定清楚表达。
把诊断还原成来源图
借用检查错误通常同时标出首次借用、冲突操作和后续使用。逐行改标注容易把错误推到别处;先恢复来源关系,才能判断需要缩短借用、分离生命周期还是改变所有权。
- 标出每个所有者的创建与销毁边界。
- 从每条引用回溯到具体所有者或输入引用。
- 标出引用的最后一次使用以及所有冲突访问。
- 对返回值和存储字段写出调用方必须满足的区域。
若来源图没有一条从返回引用通向仍有效数据的路径,语法调整不会修复设计。若路径存在但两个无关输入被放进同一关系,就应放宽标注;若共享与独占访问重叠,则应改变使用顺序或数据结构。
编译器建议常能指出可行的局部改动,但它不知道 API 的所有权意图。接受建议前,应确认它没有把借用改成不必要的克隆、泄漏或更严格的公开边界。
最小复现应保留所有者、借用和最后一次使用,而删除无关业务代码。这样既能让诊断更清楚,也能验证修改解决的是同一个区域约束。
延伸阅读
4个问题 · 1 道输出预测题 · 1 道找错题