Appearance
12.4 生命周期标记
在 Rust 中,生命周期是一种内存管理机制,用于确定引用在程序执行期间应保持有效的时间。
对一个函数内部的生命周期进行分析,Rust 编译器可以很好地解决。但是,当生命周期跨函数的时候,就需要一种特殊的生命周期标记符号了。
12.4.1 函数的生命周期标记
示例如下:
rust
struct T {
member: i32,
}
fn test<'a>(arg: &'a T) -> &'a i32 {
&arg.member
}
fn main() {
let t = T { member : 0 }; //------- 't ------
let x = test(&t); //-- 'x ---- |
println!("{:?}", x); // | |
} //-- 'x ---- 't –--
生命周期符号使用单引号 '
开头,后面跟一个合法的名字。生命周期标记和泛型类型参数是一样的,都需要先声明后使用。 在上面这段代码中,尖括号里面的 'a
是声明一个生命周期参数,它在后面的参数和返回值中被使用。
前面提到的借用指针类型都有一个生命周期泛型参数,它们的完整写法应该是 &'a T
、&'a mut T
,只不过在做局部变量的时候,生命周期参数是可以省略的。
生命周期之间有重要的包含关系。如果生命周期 'a
比 'b
更长或相等,则记为 'a: 'b
,意思是 'a
至少不会比 'b
短,英语读做“lifetime a outlives lifetime b”。 对于借用指针类型来说,如果 &'a
是合法的,那么 'b
作为 'a
的一部分,&'b
也一定是合法的。
另外,'static
是一个特殊的生命周期,它代表的是这个程序从开始到结束的整个阶段,所以它比其他任何生命周期都长。 这意味着,任意一个生命周期'a
都满足 'static:'a
。
在上面这个例子中,如果我们把变量 t
的真实生命周期记为't
,那么这个生命周期't
实际上是变量t
从“出生”到“死亡”的区间,即从第10
行到第13
行。 在函数被调用的时候,它传入的实际参数是 &t
,它是指向 t
的引用。那么可以说,在调用的时候,这个泛型参数'a
被实例化为了't
。 根据函数签名,基于返回类型的生命周期与参数是一致的,可以推理出test
函数的返回类型是 &'t i32
。 如果我们把 x
的生命周期记为 'x
,那么 'x
代表的就是从第11
行到第13
行。let x = text(&t);
语句实际上是把 &'t i32
类型的变量赋值给 &'x i32
类型的变量。 这个赋值是否合理呢?它应该是合理的。因为这两个生命周期的关系是 't: 'x
。 test
返回的那个指针在 't
这个生命周期范围内都是合法的, 处于一个被 't
包围的更小范围的生命周期内,它当然也是合法的。所以,上面这个例子即使省略生命周期标记也是可以编译通过的。
接下来,我们把上面这个例子稍作修改,让test
函数有两个生命周期参数,其中一个给函数参数使用,另外一个给返回值使用:
rust
fn test<'a, 'b>(arg: &'a T) -> &'b i32
{
&arg.member
}
编译时果然出了问题,在&arg.member
这一行,报了生命周期错误。这是为什么呢?因为这一行代码是把&'a i32
类型赋值给&'b i32
类型。 'a
和'b
有什么关系?答案是什么关系都没有。所以编译器觉得这个赋值是错误的。怎么修复呢?指定'a:'b
就可以了。 'a
比'b
“活”得长,自然,&'a i32
类型赋值给&'b i32
类型是没问题的。验证如下:
rust
fn test<'a, 'b>(arg: &'a T) -> &'b i32
where 'a:'b
{
&arg.member
}
经过这样的改写后,我们可以认为,在 test
函数被调用的时候,生命周期参数 'a
和 'b
被分别实例化为了 't
和 'x
。它们刚好满足了 where 条件中的 't:'x
约束。 而&arg.member
条表达式的类型是 &'t i32
,返回值要求的是 &'x i32
类型,可见这也是合法的。所以test
函数的生命周期检查可以通过。
上述示例是读者比较难理解的地方。以下两种写法都是可行的:
rust
fn test<'a>(arg: &'a T) -> &'a i32
fn test<'a, 'b>(arg: &'a T) -> &'b i32 where 'a:'b
这里的关键是,Rust 的引用类型支持协变性。生命周期本质上是一个时间区间,生命周期参数是泛型参数,可以被推断为具体的生命周期。
我们再看一个例子。它有两个引用参数,共享同一个生命周期标记:
rust
fn select<'a>(arg1: &'a i32, arg2: &'a i32) -> &'a i32 {
if *arg1 > *arg2 {
arg1
} else {
arg2
}
}
fn main() {
let x = 1;
let y = 2;
let selected = select(&x, &y);
println!("{}", selected);
}
在 select
函数中:
- 函数签名要求:两个参数和返回值共享同一个生命周期标记
'a
- 实际调用时:
&x
和&y
可能有不同的潜在生命周期 - 协变性机制:编译器通过协变性,将两个引用的生命周期都"收缩"到一个共同的生命周期
'a
- 生命周期推断:
'a
被推断为所有参数生命周期的交集(最短的那个) - 安全保证:返回值
selected
的生命周期是'a
,不会超过任何输入参数的实际生命周期
关键要点
- 协变性:长生命周期可以安全地用在需要短生命周期的地方
- 生命周期推断:编译器自动找到最合适的公共生命周期
- 安全性:返回值的生命周期受限于最短的输入参数生命周期
- 灵活性:不同生命周期的引用可以传递给相同生命周期的函数
12.4.2 类型的生命周期标记
如果自定义类型中有成员包含生命周期参数,那么这个自定义类型也必须有生命周期参数。示例如下:
rust
struct Test<'a> {
member: &'a str
}
在使用 impl 的时候,也需要先声明再使用:
rust
impl<'t> Test<'t> {
fn test<'a>(&self, s: &'a str) {
}
}
impl 后面的那个't
是用于声明生命周期参数的,后面的 Test<'t>
是在类型中使用这个参数。如果有必要的话,方法中还能继续引入新的泛型参数。
如果在泛型约束中有where T:'a
之类的条件,其意思是,类型 T 的所有生命周期参数必须大于等于'a
。
'static
生命周期
where T:'static
是一种泛型约束,它表示类型 T 必须是静态生命周期的。 类型 T 里面不包含任何指向短生命周期的借用指针,意思是要么完全不包含任何借用,要么可以有指向'static
的借用指针。
静态生命周期是 Rust 中一种特殊的生命周期,Rust 中的全局变量(静态变量)、常量(Const)、字符串字面量具有静态生命周期,贯穿整个程序进程的生命周期。
例如,在以下代码中,foo
函数接受一个泛型类型参数 T
,具有 T:'static
约束。 这意味着在 foo
函数中,任何对 T
类型中引用的使用,其生命周期都必须不早于 foo
函数执行期间并且不晚于整个程序执行期间。
rust
fn foo<T: 'static>(_x: T) {
// println!("{}", x);
}
fn main() {
let s = "hello";
let n = 42;
// 我们可以将具有 `'static` 生命周期的引用作为参数传递给 `print_static` 函数
foo(s);
// 对于不具有 `'static` 生命周期的引用,编译器会在编译时报错
foo(&n); // 编译错误!
}
以上程序在运行时,会发生错误:
error[E0597]: `n` does not live long enough