Skip to content

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: 'xtest返回的那个指针在'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'yselect函数的形式参数要求的是同样的生命周期,而实际参数是两个不同生命周期的引用,这个类型之所以可以匹配成功,就是因为生命周期的协变特性。 编译器可以把&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

Released under the MIT License