Rust 生命周期用法与示例集锦
前言
在 Rust 里,引用不拥有数据,只是「指向某处已有数据」的别名。
编译器必须保证:在引用被使用的整个过程中,那份数据始终合法,否则就会出现悬垂引用。
生命周期描述的就是「这段引用(或包含引用的类型)被认为有效的一段区间」,它是静态分析里的概念,用来把上述安全要求写进类型系统。
生命周期参数(如 'a)是给不同引用之间的关系起的标签,用来表达「谁不能比谁先死」「返回值借自哪一个参数」等约束。
多数时候编译器能推断生命周期,只有签名或数据结构里关系变复杂时,才需要你手写标注。
它与所有权、借用规则一起工作:所有权决定谁负责释放,借用检查决定同一时刻可如何使用引用,生命周期则把「引用能活多久」说清楚,借用检查器正是据此做验证。
本文用大量短例说明常见写法,读者若已会基本所有权与借用,可按小节查阅。
示例以 rustc 稳定版常见语义为准,具体报错措辞可能随版本略有差异。
若只想快速定位,可按一级标题跳转,例如基础、类型与方法、约束与寿命、进阶、与语言特性、排错与实践。
基础
动机
下面这段在逻辑上「没问题」,但 Rust 会拒绝编译,因为编译器无法证明 r 指向的数据在 r 被使用期间始终有效。
下面代码演示「悬垂风险」:内部创建的 y 在块结束后失效,却可能通过返回值被外界长期持有。
1 | // 不能通过编译的示意(省略生命周期时编译器会推断失败或给出矛盾) |
生命周期标注不改变运行期行为,只帮助类型检查器表达「这些引用必须同时有效」的约束。
显式标注
函数签名里,生命周期参数写在 ' 后接名字,表示「某个引用存活的那段区间」。
同一名字出现在多个位置时,表示它们必须兼容(通常理解为「一样长」或「被约束为子区域」)。
下面示例声明返回值与参数 x 共享同一生命周期,这样调用方知道返回的引用不会比 x 活得更久。
1 | fn first<'a>(x: &'a str, _y: &str) -> &'a str { |
多个生命周期可以区分「第一个参数」与「第二个参数」谁约束了返回值。
下面函数返回的两个切片都来自同一输入 s,因此返回值与 s 使用同一 'a。
1 | fn split_at_comma<'a>(s: &'a str) -> (&'a str, &'a str) { |
省略规则
写函数时常省略生命周期,编译器按三条规则补全(简化记忆:输入多条则输出必须显式标;单条输入可推出输出;方法里 self 参与推断)。
下面代码省略标注仍可编译,因为只有一个引用输入,输出被规则推断为与之相同。
1 | fn len(s: &str) -> usize { |
一旦签名里出现歧义(例如多个引用入参且返回值是引用),就需要手写 'a 等,见下文「类型与方法」中的结构体与多参数函数。
类型与方法
结构体字段
若结构体里存了引用,每个引用字段都要命名生命周期,且结构体名上也要声明这些参数。
下面 Excerpt 持有对字符串的借用,字段与结构体共同标注 'a。
1 | struct Excerpt<'a> { |
方法里的生命周期
方法的 self 为引用时,常与返回的借用一起出现在生命周期推断里。
下面 get 返回的引用与 &self 同生命周期,表明不会返回比实例活得更久的引用(此处指向内部借用的切片元素)。
1 | struct Buffer<'a> { |
与泛型参数并存
结构体可同时带生命周期与类型参数,impl 块头要重复声明。
下面缓存「对切片中元素的引用」,元素类型为 T,寿命为 'a。
1 | struct SliceCache<'a, T> { |
与 trait 泛型
trait 可带生命周期参数,实现处要与类型声明一致。
下面 trait 在 'a 上提供对缓冲区的只读视图(示意)。
1 | trait View<'a> { |
约束与寿命
静态生命周期
'static 表示「与整个程序一样久」,字符串字面量类型即为 &'static str。
下面把字面量赋给 &'static str,可存入要求静态引用的地方(例如某些全局或跨线程边界 API)。
1 | const GREETING: &'static str = "hello"; |
注意:不是「栈上数据活多久」的意思;'static 更多描述「数据地址与内容在程序生存期内有效」这一类情况。
多条约束
泛型里常见 T: 'a,表示类型 T 里不能包含活不过 'a 的借用(类型必须至少能活到 'a)。
下面容器在 'a 内保存 T 的引用,要求 T 本身在该区间有效。
1 | struct RefOpt<'a, T: 'a> { |
where 子句可并列多条生命周期与 trait 约束,复杂签名更清晰。
进阶
子类型与协变
引用具有协变性:&'long T 可视为 &'short T 的子类型(寿命缩短是安全的)。
函数指针参数位置会涉及逆变,日常写业务代码时先记住「缩短引用寿命通常更严、需显式标注」即可,遇到奇怪签名再查参考手册。
高阶与 HRTB
有时需要表达「对任意生命周期 'a 都成立」的约束,这时在 where 里使用 for<'a>,称为高阶 trait 约束(HRTB)。
典型例子是要求闭包能接受任意寿命的引用,而不是固定某一个 'a。
下面示例要求 F 对任意 'a 都能把 &'a str 映射为 usize(示意 API,具体场景如解析器回调)。
1 | fn apply_str<F>(f: F, s: &str) -> usize |
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 | struct Holder<'a> { |
更复杂的「高阶生命周期」出现在返回引用闭包等场景,一般优先改写为泛型或显式 trait 对象边界。
结合迭代器
许多迭代器适配器会保留底层引用的生命周期,例如 iter() 产生 Item = &'a T。
下面函数在 'a 上遍历,返回的迭代器项生命周期与切片一致。
1 | fn iter_values<'a>(v: &'a [i32]) -> impl Iterator<Item = &'a i32> + 'a { |
排错与实践
常见编译错误与修法
错误 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 | trait Reader { |
模式小结
- 先区分「拥有」与「借用」,只在必须共享借用时引入生命周期参数。
- 函数里多入参引用且返回引用时,优先写清
'a与返回值关联。 - 结构体存引用时,字段、结构体头、
impl块三处生命周期要一致声明。 - 遇到推断失败,把复杂签名拆到
where,或用拥有类型消除借用。 - 字面量与全局常量多涉
'static,不要把普通局部引用强行标成'static。
验证
本地可用 rustc 对单文件做快速检查(示例命令如下,文件名按实际修改)。
下面命令在工程外快速编译一段示例文件,便于对照报错与修正(需已安装 Rust 工具链)。
1 | rustc --edition 2021 your_snippet.rs |
若使用 cargo,把示例放进 src/bin 或测试里运行 cargo check 更接近真实项目配置。