Rust 生命周期用法与示例集锦

前言

在 Rust 里,引用不拥有数据,只是「指向某处已有数据」的别名。
编译器必须保证:在引用被使用的整个过程中,那份数据始终合法,否则就会出现悬垂引用。
生命周期描述的就是「这段引用(或包含引用的类型)被认为有效的一段区间」,它是静态分析里的概念,用来把上述安全要求写进类型系统。

生命周期参数(如 'a)是给不同引用之间的关系起的标签,用来表达「谁不能比谁先死」「返回值借自哪一个参数」等约束。
多数时候编译器能推断生命周期,只有签名或数据结构里关系变复杂时,才需要你手写标注。
它与所有权、借用规则一起工作:所有权决定谁负责释放,借用检查决定同一时刻可如何使用引用,生命周期则把「引用能活多久」说清楚,借用检查器正是据此做验证。

本文用大量短例说明常见写法,读者若已会基本所有权与借用,可按小节查阅。
示例以 rustc 稳定版常见语义为准,具体报错措辞可能随版本略有差异。
若只想快速定位,可按一级标题跳转,例如基础、类型与方法、约束与寿命、进阶、与语言特性、排错与实践。

基础

动机

下面这段在逻辑上「没问题」,但 Rust 会拒绝编译,因为编译器无法证明 r 指向的数据在 r 被使用期间始终有效。

下面代码演示「悬垂风险」:内部创建的 y 在块结束后失效,却可能通过返回值被外界长期持有。

1
2
3
4
5
// 不能通过编译的示意(省略生命周期时编译器会推断失败或给出矛盾)
fn broken() -> &i32 {
let y = 42;
&y
}

生命周期标注不改变运行期行为,只帮助类型检查器表达「这些引用必须同时有效」的约束。

显式标注

函数签名里,生命周期参数写在 ' 后接名字,表示「某个引用存活的那段区间」。
同一名字出现在多个位置时,表示它们必须兼容(通常理解为「一样长」或「被约束为子区域」)。

下面示例声明返回值与参数 x 共享同一生命周期,这样调用方知道返回的引用不会比 x 活得更久。

1
2
3
fn first<'a>(x: &'a str, _y: &str) -> &'a str {
x
}

多个生命周期可以区分「第一个参数」与「第二个参数」谁约束了返回值。

下面函数返回的两个切片都来自同一输入 s,因此返回值与 s 使用同一 'a

1
2
3
4
5
6
fn split_at_comma<'a>(s: &'a str) -> (&'a str, &'a str) {
match s.split_once(',') {
Some((a, b)) => (a, b),
None => (s, ""),
}
}

省略规则

写函数时常省略生命周期,编译器按三条规则补全(简化记忆:输入多条则输出必须显式标;单条输入可推出输出;方法里 self 参与推断)。

下面代码省略标注仍可编译,因为只有一个引用输入,输出被规则推断为与之相同。

1
2
3
fn len(s: &str) -> usize {
s.len()
}

一旦签名里出现歧义(例如多个引用入参且返回值是引用),就需要手写 'a 等,见下文「类型与方法」中的结构体与多参数函数。

类型与方法

结构体字段

若结构体里存了引用,每个引用字段都要命名生命周期,且结构体名上也要声明这些参数。

下面 Excerpt 持有对字符串的借用,字段与结构体共同标注 'a

1
2
3
4
5
6
7
8
9
10
struct Excerpt<'a> {
text: &'a str,
start: usize,
}

impl<'a> Excerpt<'a> {
fn bump(&mut self, n: usize) {
self.start = self.start.saturating_add(n);
}
}

方法里的生命周期

方法的 self 为引用时,常与返回的借用一起出现在生命周期推断里。

下面 get 返回的引用与 &self 同生命周期,表明不会返回比实例活得更久的引用(此处指向内部借用的切片元素)。

1
2
3
4
5
6
7
8
9
struct Buffer<'a> {
items: &'a [i32],
}

impl<'a> Buffer<'a> {
fn get(&self, i: usize) -> Option<&'a i32> {
self.items.get(i)
}
}

与泛型参数并存

结构体可同时带生命周期与类型参数,impl 块头要重复声明。

下面缓存「对切片中元素的引用」,元素类型为 T,寿命为 'a

1
2
3
4
5
6
7
8
9
10
struct SliceCache<'a, T> {
data: &'a [T],
pos: usize,
}

impl<'a, T> SliceCache<'a, T> {
fn peek(&self) -> Option<&'a T> {
self.data.get(self.pos)
}
}

与 trait 泛型

trait 可带生命周期参数,实现处要与类型声明一致。

下面 trait 在 'a 上提供对缓冲区的只读视图(示意)。

1
2
3
4
5
6
7
8
9
10
11
12
13
trait View<'a> {
fn bytes(&self) -> &'a [u8];
}

struct Packet<'a> {
buf: &'a [u8],
}

impl<'a> View<'a> for Packet<'a> {
fn bytes(&self) -> &'a [u8] {
self.buf
}
}

约束与寿命

静态生命周期

'static 表示「与整个程序一样久」,字符串字面量类型即为 &'static str

下面把字面量赋给 &'static str,可存入要求静态引用的地方(例如某些全局或跨线程边界 API)。

1
const GREETING: &'static str = "hello";

注意:不是「栈上数据活多久」的意思;'static 更多描述「数据地址与内容在程序生存期内有效」这一类情况。

多条约束

泛型里常见 T: 'a,表示类型 T 里不能包含活不过 'a 的借用(类型必须至少能活到 'a)。

下面容器在 'a 内保存 T 的引用,要求 T 本身在该区间有效。

1
2
3
struct RefOpt<'a, T: 'a> {
r: Option<&'a T>,
}

where 子句可并列多条生命周期与 trait 约束,复杂签名更清晰。

进阶

子类型与协变

引用具有协变性:&'long T 可视为 &'short T 的子类型(寿命缩短是安全的)。
函数指针参数位置会涉及逆变,日常写业务代码时先记住「缩短引用寿命通常更严、需显式标注」即可,遇到奇怪签名再查参考手册。

高阶与 HRTB

有时需要表达「对任意生命周期 'a 都成立」的约束,这时在 where 里使用 for<'a>,称为高阶 trait 约束(HRTB)。
典型例子是要求闭包能接受任意寿命的引用,而不是固定某一个 'a

下面示例要求 F 对任意 'a 都能把 &'a str 映射为 usize(示意 API,具体场景如解析器回调)。

1
2
3
4
5
6
fn apply_str<F>(f: F, s: &str) -> usize
where
F: for<'a> Fn(&'a str) -> usize,
{
f(s)
}

impl Trait 与生命周期

返回 impl Trait 时,若内部仍含借用,编译器会把隐藏类型里的生命周期一并检查。
需要「返回某种迭代器且项为 &'a T」时常写作 impl Iterator<Item = &'a T> + 'a

同一篇中「与语言特性」下的「结合迭代器」里的 iter_values 即是一种形式;若错误提示「需要指定显式生命周期」,多半要把 'a 同时绑到 impl Trait 与参数上。

与语言特性

异步与生命周期

async fn 会把参数捕获进生成的 Future,因此参数借用往往要活得比单次调用更长,错误信息里常见 future is not Send 或生命周期不匹配。
修法通常是改为 'static 友好类型、使用 Arc、或缩小 async 块捕获范围;细节与项目运行时有关,此处只点出「异步会拉长借用检查视野」。

闭包与生命周期

闭包捕获引用时,生成的结构体往往带生命周期;若要把闭包存进结构体,常需 Box<dyn Fn(...) + 'a> 这类形式。

下面在 'a 内保存一个只读访问闭包,闭包本身不能超过 'a

1
2
3
4
5
6
7
8
9
struct Holder<'a> {
f: Box<dyn Fn() -> i32 + 'a>,
}

fn make<'a>(x: &'a i32) -> Holder<'a> {
Holder {
f: Box::new(|| *x),
}
}

更复杂的「高阶生命周期」出现在返回引用闭包等场景,一般优先改写为泛型或显式 trait 对象边界。

结合迭代器

许多迭代器适配器会保留底层引用的生命周期,例如 iter() 产生 Item = &'a T

下面函数在 'a 上遍历,返回的迭代器项生命周期与切片一致。

1
2
3
fn iter_values<'a>(v: &'a [i32]) -> impl Iterator<Item = &'a i32> + 'a {
v.iter()
}

排错与实践

常见编译错误与修法

错误 A:does not live long enough
通常是返回值或字段引用了局部变量。
修法:改为拥有数据(String 而非 &str)、缩短作用域,或把生命周期正确连到调用方已有引用上。

错误 B:lifetime may not live long enough
常出现在多分支返回不同借用,或异步/dyn 边界。
修法:统一返回类型(如 .to_string() 升为拥有型)、给枚举包装不同来源,或显式标注让各分支一致。

错误 C:trait 对象 dyn Trait + 'a
dyn Trait 内含引用时,需要 dyn Trait + 'a 标明对象内引用至少活多久。

下面展示带生命周期的 trait object 类型写法(具体 trait 可替换为项目内定义)。

1
2
3
4
5
6
7
trait Reader {
fn chunk(&self) -> &[u8];
}

fn as_reader<'a>(r: &'a (dyn Reader + 'a)) -> &'a (dyn Reader + 'a) {
r
}

模式小结

  1. 先区分「拥有」与「借用」,只在必须共享借用时引入生命周期参数。
  2. 函数里多入参引用且返回引用时,优先写清 'a 与返回值关联。
  3. 结构体存引用时,字段、结构体头、impl 块三处生命周期要一致声明。
  4. 遇到推断失败,把复杂签名拆到 where,或用拥有类型消除借用。
  5. 字面量与全局常量多涉 'static,不要把普通局部引用强行标成 'static

验证

本地可用 rustc 对单文件做快速检查(示例命令如下,文件名按实际修改)。

下面命令在工程外快速编译一段示例文件,便于对照报错与修正(需已安装 Rust 工具链)。

1
rustc --edition 2021 your_snippet.rs

若使用 cargo,把示例放进 src/bin 或测试里运行 cargo check 更接近真实项目配置。