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
这个函数引入了一个生命周期标记,两个参数以及返回值都是用的这个生命周期标记。同时我们注意到,在调用的时候,传递的实参其实是具备不同的生命周期的。 x
的生命周期明显大于y
的生命周期,&x
可存活的范围要大于&y
可存活的范围,我们把它们的实际生命周期分别记录为'x
和'y
。 select
函数的形式参数要求的是同样的生命周期,而实际参数是两个不同生命周期的引用,这个类型之所以可以匹配成功,就是因为生命周期的协变特性。 编译器可以把&x
和&y
的生命周期都缩小到某个生命周期'a
以内,且满足'x:'a
,'y:'a
。 返回的selected
变量具备'a
生命周期,也并没有超过'x
和'y
的范围。所以,最终的生命周期检查可以通过。
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