引言
这是Rust九九八十一难第十六篇。上篇聊了下Rust有哪些危险操作,有个操作是无脑Clone的风险,这时候可能用到引用和’a生命周期注解(Lifetime annotations)。那么问题来了,有时候必须加,有时不用,到底什么时候用’a,生命周期的计算规则是什么等等。这篇就整理下生命周期注解的使用。
一、简单了解和入门
使用之前,先做个入门。Lifetime annotations(’a,’b等)像是一种约束,只是告诉编译器引用之间的有效期关系,编译器以此和borrow-checker,保证引用不会比引用数据活的更久。有点类似学校的某班点名册,学生是数据,点名册是引用,他俩关系是学生毕业了,这本点名册就要废弃。
1、函数返回引用
下面的函数接受俩字符串切片,返回返回其中较长的一个
错误使用:
1 | fn longest(x: &str, y: &str) -> &str { |
编译报错:Missing lifetime specifier [E0106]
正确使用:
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { |
这里&'a str表示&str在用在某个生命周期 'a 内有效,x 和 y 的引用都有生命周期 'a,函数返回一个同样具有 'a 生命周期的引用。编译器知道后,保证x和y活多久,只要它们都活过同一段 'a,那么返回的引用就是安全的。
2、struct\enum使用生命周期注解
用结构体存引用,这必须给struct加生命周期注解。比如下面的例子:
1 |
|
Book 存有引用字段,Rust需要知道引用在哪段时间有效(防止悬垂引用、推导借用规则等),于是 Book<'a> 中 'a 表示:这个 struct 所持的引用至少活到 'a,且 Book<'a> 的实例本身不能比 'a 活得更久。也就是说:
'a是一个 约束条件:Book 内部引用必须有效- struct 的生命周期 ≤
'a
二、Lifetime annotations命名规则
1、规则和限制
必须以
'开头,所有生命周期名称都必须以单引号'开头,比如:’a,’static。'a、'b、'c是最常见的生命周期名可以是多个单词或多个字符(但必须合法的标识符):
1
fn parse<'de>(s: &'de str) { } //常见于 Serde 的 'de
必须是合法标识符字符(字母、数字、下划线),但不能以数字开头:
- 合法: ‘file、’input1 、’_a
- 不合法:’1abc、’’abc // 双引号
'static(内置生命周期),可以使用'static,它不是关键字,但它的意义被语言定义。'_'(匿名生命周期),从 Rust 1.34 开始允许,用于减少显式写法1
fn foo<'_>(x: &'_ str) {}
Rust 没有任何关键字被禁止用作生命周期名。但随便用关键字会降低可读性。
2、一些建议
如果不需要写生命周期,就别写,写了会传染
简单场景用
'a足够,不要过度设计多个生命周期时,用
'a'b'c,除非语义重要不关心具体生命周期用’_
程序整个生命周期,’static
生命周期与类型参数顺序:先
'a后T1
fn f<'a, T>(x: &'a T) {}
特定场景
- 解析数据:用
'de和'input组合
1
2
3fn parse<'de, 'input>(src: &'input str) -> Result<T<'de>, E> {
...
}- 上下文:’ctx
- 配置:’cfg
- 字符串来源:’s
- 解析数据:用
默认使用
'a', 'b', 'c'。只有当它们导致混淆,或者你需要为特定的外部数据源提供明确的上下文时,才考虑使用更长的、描述性的名称(如'repo,'src')。
三、生命周期注解如何计算存活时间
Rust 生命周期注解本身并不计算或控制引用的存活时间,它是描述性的。它的作用是告诉 Borrow Checker不同引用之间的生命周期关系,从而让编译器能够检查引用是否有效,防止悬垂引用的产生。
Rust lifetime = 取“引用被使用的区间”与“被借用对象的存活区间”的交集,并确保借用不超过对象范围,如果涉及多个借用,则返回引用的生存区间必须是输入引用交集中的某个合法子集。通过几个例子说明下:
示例1:引用的生命周期计算(显式注解版, 平常不用加,这里为了展示方便)
1
2
3
4
5
6fn main<'main>() {
let x: i32 = 5; // x: 'main
let r: &'main i32 = &x; // r: 'main
println!("{}", r); // r 最后一次使用
} // 'main 在函数结束1
2
3x: ─────────────────────────── 'main
r: ────────────────────── 'main
^开始借用 ^最后使用生命周期
'main完全包含r的使用区间,因此合法。示例2:返回引用必须绑定输入(显式注解版)
1
2
3fn get_ref<'a>(x: &'a i32) -> &'a i32 {
x
}1
2x (input): ──────────────────── 'a
return: ───────────────── 'a返回引用直接继承
'a,不能产生新的生命周期。示例3:两个输入生命周期不同,但返回其中之一
1
2
3fn pick<'a, 'b>(a: &'a i32, b: &'b i32) -> &'a i32 {
a
}1
2
3'a: ───────────────────────────────
'b: ───────────────
ret: ─────────────────────────────── (和a一样)示例4:返回交集例子
- struct例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15impl<'a, 'b> Pair<'a, 'b> {
// 允许只有当 'a 与 'b 有重叠时
fn shortest<'c>(&'c self) -> &'c str
where
'a: 'c,
'b: 'c,
{
if self.left.len() < self.right.len() {
self.left
} else {
self.right
}
}
}1
2
3
4'a: ─────────────────────────────
'b: ───────────────────────
'c: ─────────────── (交集区域)- 函数例子
1
2
3
4
5
6
7fn overlap<'a, 'b, 'c>(a: &'a str, b: &'b str) -> &'c str
where
'a: 'c,
'b: 'c,
{
if a.len() < b.len() { a } else { b }
}1
2
3
4'a: ───────────────────────
'b: ─────────────────────
'c (交集): ─────────────返回值
'c就是'a ∩ 'b的交集范围
四、使用场景:哪些情况必须使用vs哪些不用
既然生命周期注解有这么重要,为什么好多情况不加也没问题呢,到底什么情况下需要?这就涉及到生命周期省略规则。
1、Rust在这些场景下能自动推断生命周期,无需手动加标注。
a、所有输入引用各自都有一个推断到的生命周期
函数参数里每个 &T,如果没写生命周期,编译器会自动给它加一个,但不返回引用时或者返回值不是引用。
- 例如:
fn foo(x: &str, y: &str)等价于fn foo<'a, 'b>(x: &'a str, y: &'b str)。
b、只有一个输入引用,则输出引用用它的生命周期
如果函数 有且只有一个输入引用,并且返回引用, 那么返回值的生命周期会自动和这个输入一样。这是最安全的默认推断:如果返回一个引用,那么它必须依赖于唯一的输入引用
- 例如:
fn foo(x: &str) -> &str等价于fn foo<'a>(x: &'a str) -> &'a str。
c、方法中有 &self / &mut self,输出引用默认跟 self 的生命周期
假设方法是借用(而非所有权转移),返回的任何引用最有可能指向方法调用者(self)所拥有的数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20struct Owner {
data: String,
}
impl Owner {
// 原始代码:没有手动标注
fn get_data_ref(&self) -> &str {
&self.data // 返回对自身数据的引用
}
}
// 编译器推断:等价于
/*
impl<'a> Owner {
fn get_data_ref(&'a self) -> &'a str {
&self.data
}
}
*/
// 约束:返回的 &str 的生命周期 'a 与 &self 的生命周期 'a 相同。
d、结构体没有引用字段
1 | struct Foo { x: i32 } |
2、必须加生命周期标注
a、结构体(或枚举)里含有引用字段
1 | struct Foo<'a> { |
结构体持有引用必须明确生命周期,否则编译器不知道引用活多久。
b、函数/方法返回的引用来自多个输入引用(多对一),编译器无法判断返回的是谁
1 | fn choose(x: &str, y: &str) -> &str { ... } |
这会报错,因为编译器不知道返回值应该跟 'x 还是 'y。必须写:
1 | fn choose<'a>(x: &'a str, y: &'a str) -> &'a str //// 约束:返回的引用 'a 依赖于 x 和 y 中生命周期最短的那个。 |
c、方法返回的引用不属于 self,但有多个输入引用
1 | fn pick<'a>(&self, other: &'a str) -> &str { |
如果不写:
1 | fn pick(&self, other: &str) -> &str |
编译器会以为输出是 self 的生命周期(规则 3),导致错误。
3、特殊场景
3.1、 声明生命周期的“长短关系” 'a: 'b
你有两个输入生命周期 'a 和 'b,但你要保证 'a >= 'b('a 活得至少跟 'b 一样久),否则返回 'b 的引用可能会出错。
1 | fn choose_first<'a, 'b>(x: &'a str, _y: &'b str) -> &'b str |
1 | struct Mapper<'a, 'b> { |
'a: 'b 是 Rust 生命周期 subtyping(子类型)约束的语法,用来表达:
生命周期
'a必须比'b活得更久(或至少一样久)
换句话说:'a≥'b
3.2、trait object 中的生命周期(如 Box<dyn Trait + 'a>)
这个 Box 必须至少活 'a 那么久,否则其中的引用就可能悬垂。通常情况下不需要,但在某些特定场景下是必须的。在绝大多数情况下,当写 Box<dyn Printer> 时,编译器会自动添加生命周期参数。
Box<dyn Trait>等同于 'static,不能放非 'static 引用。只有当 Trait Object 内部的类型依赖于一个非 'static' 的,即局部作用域的生命周期时,才必须手动添加 'a 来约束它。
- 必须加举例:
1 | trait Printer { |
上面例子创建一个 StrPrinter,并且这个对象内部封装了一个生命周期短于 'static' 的引用。省略 'a' 会导致编译器默认采用 'static' 约束,从而引发安全错误.
- 不用加举例
1 |
|
3.3、HRTB 必须加 for<'a>
HRTB(Higher-Rank Trait Bounds,高阶生命周期约束)
HRTB 使用 for<'a> 语法,是因为它用于声明或约束一个类型(通常是闭包或函数指针)的能力,即该类型对于任意传入的生命周期都是有效的。
- 普通函数签名只能用一个固定生命周期,比如:
1 | //这里的 fn(&i32) 实际上隐含了某个生命周期 'x |
- 如果你想让
f可以接受 任意生命周期 的引用,就需要 HRTB:
1 | fn call_fn<F>(f: F) |
这里 for<'a> 告诉 Rust:f 对 所有生命周期 'a 的引用 都有效
举个例子
1
2
3
4
5
6
7
8
9
10
11fn call_with_i32<F>(f: F)
where
F: for<'a> Fn(&'a i32),
{
let x = 5;
let y = 10;
f(&x); // &'a i32 可以是 x 的生命周期
f(&y); // &'a i32 可以是 y 的生命周期
}
//调用,闭包 |n| println!("{}", n) 可以接受任意生命周期的 &i32
call_with_i32(|n| println!("{}", n));什么时候必须用HRTB(
for<'a>)函数接受泛型闭包或函数指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 无法省略!必须用 for<'a> 来表达对任意生命周期 'a 的适用性
fn process_multiple_borrows<F>(f: F)
where
// HRTB: 约束 F 必须对于所有生命周期 'a 都满足 Fn(&'a str) Trait
F: for<'a> Fn(&'a str) -> u32,
{
// 短命引用 L_short
let data_temp = String::from("ephemeral");
let len_temp = f(&data_temp);
println!("Temp data length: {}", len_temp);
// 长命引用 L_static
let len_static = f("static string");
println!("Static data length: {}", len_static);
}
fn main() {
let my_counter = |s: &str| s.len() as u32;
process_multiple_borrows(my_counter);
}高阶函数 / trait / async / future 的引用需要泛型生命周期时
1
2
3
4
5
6fn apply<F>(f: F)
where
F: for<'a> Fn(&'a str) -> &'a str,
{
// 可以接受任意生命周期的 &str
}
3.4 、GATs需要加’a场景
GATs (Generic Associated Types),即通用关联类型,是 Rust 语言中一项重要的、相对高级的特性。它允许 Trait(特型)的关联类型能够拥有自己的泛型参数(包括生命周期参数和类型参数),就像 Trait 本身可以拥有泛型参数一样。使用 GATs,可以定义一个关联类型 Ref,并为其引入一个生命周期参数 'a,从而明确这个引用依赖于 'a。
语法:
1 | trait MyTrait { |
在迭代器场景,Iterator::Item 是一个 独立于迭代器(self)生命周期的类型。没有GATs,不能安全地写出“返回对自身内部数据的引用”的迭代器。
举例:
1 | pub trait LendingIterator { |
Item<'a>不是一个固定类型,而是一个模板,根据'a不同得到不同的具体类型。Item<’short>会变成&’short str, Item<’long>会变成&’long str- next 返回的内容与
&self生命周期保持一致,保证安全
五、总结
本文总结了生命周期注解的使用方式、命名规则和必须使用的场景等,优点是帮助编译器更好的管理。缺点也不少,比如学习复杂;注解有传播性,一旦一个类型(struct、enum、函数)带上生命周期 'a,那么它的使用者可能也必须带上 'a,项目大,可能会反省爆炸;编译器无法推断所有场景。使用的时候,能不加就不加,仅在编译器报错/性能需求/结构设计确实要求借用时,才加生命周期注解。
如果觉得本文有用,本人公众号大鱼七成饱,辛苦点个关注吧。