Rust:生命周期

Rust:生命周期


生命周期

前一篇博客讲解了Rust的所有权机制,其中借用是非常重要的组成部分。在许多其它语言中,往往会提供空指针,空引用来表示一个引用没有指向任何位置。而Rust没有提供空借用,那么一个借用有没有可能是无效的?

逻辑上来说完全有可能,但是Rust基于生命周期机制,杜绝了这种情况。

例如:

rust 复制代码
fn main() {
    let borrow;

    {
        let s = String::from("hello");
        borrow = &s;
    }

    println!("Borrow: {}", borrow);
}

以上代码会报错,因为借用的生命周期比这个借用指向的值生命周期还要长。一旦s销毁,后面borrow就变成了垂悬引用,也就是这个引用指向了一块已经回收的空间。

这种简单的生命周期关系Rust编译器自己就可以识别,但是借用可以跨函数、跨作用域进行传递,此时编译器就无法这么轻易的判断生命周期之间的关系了。

例如:

rust 复制代码
fn longer_str(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

以上代码是生命周期最经典的案例,它传入两个字符串借用,返回两个字符串中比较长的那个字符串的借用。

但是代码报错了,它无法编译通过,因为Rust无法搞清楚这些借用之间的关系。

假设在main中这样调用:

rust 复制代码
let ret;
let s1: &str = "hello";
{
    let s2: &str = "world!";
    ret = longer_str(s1, s2);
    
    println!("{}", ret);
}
println!("{}", ret);

从外面看,编译器只知道longer_str返回了一个字符串借用,根本不知道这个借用生命周期能不能覆盖到两次println的使用。

也就是说编译器不知道目前各个借用生命周期之间的关系,导致无法进行借用规则的判断,这个时候就需要进行显式生命周期标注。


显式标注

显式标注生命周期的语法,和泛型是一样的,可用于函数、方法、结构体、Trait中,只要能用泛型的位置,几乎都可以进行生命周期标注。

Rust中,生命周期和泛型类型参数在语法层级相同,它们都作为"泛型参数"存在,但生命周期不是一种类型。

函数

语法:

rust 复制代码
fn func<'a, 'b ...>(arg_1: &'a type, arg_2: &'b mut type) -> &'a str {}

生命周期参数和泛型一样,需要在<>内部进行声明,后续才能使用。生命周期参数以单引号开头,往往以单个小写字母命名,当然不是单个字母也行,这只是社区的习惯。

在函数签名中,如果参数或返回值是借用,那么可以使用前面声明过的生命周期参数,使用时固定放在&的后面。比如&'a str&'b mut str

生命周期参数不会影响运行时的代码,不论是从效率还是从逻辑上,它只影响编译器对借用之间关系的检查。

当在<>内声明一个生命周期参数,它不代表任何含义。当在函数签名中使用生命周期参数,此时代表该参数的生命周期一定大于等于声明的这个生命周期。

这听起来有点不好理解,用刚才的例子来讲解:

rust 复制代码
fn longer_str<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

longer_str函数中,声明了一个生命周期参数'a,此时它还没有任何含义。随后分别给xy两个借用使用了'a生命周期参数,这表示:借用xy的生命周期都会大于等于'a,因此'a就是xy生命周期的较小值。

当一个生命周期参数作用于多个函数参数,这表示这些参数都大于等于这个生命周期,因此生命周期就是所有参数生命周期的最小值。假设 'a 同时受 'x'y'z限制,因此 'a 的生存范围不会超过三者中生命期较短的那个。从效果上看,相当于 'a = min('x, 'y, 'z)

这里要着重强调的是,生命周期一开始没有含义,只是一个占位符,只有通过具体的参数使用后,它才有具体意义。很多人先入为主,认为是生命周期参数去限制借用,恰恰相反,是借用在描述生命周期参数。

用刚才的例子再次加深理解,'a在声明的时候没有任何意义,只有将'a用在xy参数后,'a才有具体意义:xy生命周期的较小值。

那么最后把已经有意义'a作用于返回值,表示:这个返回值的生命周期至少是xy生命周期的较小值。这样就用一个生命周期参数把函数参数和返回值这几个借用的关系联系了起来。

回到外部调用函数的位置,看看编译器又是如何分析的:

rust 复制代码
let ret;
let s1: &str = "hello";
{
    let s2: &str = "world!";
    ret = longer_str(s1, s2);
    
    println!("{}", ret);
}
println!("{}", ret);

依然是刚才的调用逻辑,现在将s1s2两个借用的生命周期标注出来:

rust 复制代码
let s1: &str = "hello";
'x {
    'block {
        let s2: &str = "world!";
        'y {
            ret = longer_str(s1, s2);
            println!("{}", ret); // success
        }
    }
    println!("{}", ret); // error
}

以上代码相当于编译器视角下的代码,把生命周期标注了出来。

s1的生命周期是'xs2的生命周期是'y,而代码块的生命周期是'block

longer_str传入s1s2的时候,'a = min('x, 'y),因此'a = 'y,这样函数的'a生命周期就被确定下来了。

而函数返回值的生命周期也是'a,因此编译器就知道ret的生命周期也是'a,也就是'y。第一次println!'y生命周期内,合法。而第二次在生命周期外,所以报错,编译不通过。

一个例子总是片面的,再看第二个案例:

rust 复制代码
fn before_str<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
    x
}

fn main(){
	let ret;
	let s1: &str = "hello";
	{        
	    let s2: &str = "world!";
	    ret = before_str(s1, s2);
	    println!("{}", ret);
	}
	println!("{}", ret);
}

函数before_str固定返回第一个字符串的借用,因此x和返回值的生命周期都是'a,而y的生命周期是'b

以上代码不会发生任何报错,编译通过。分析一下:

rust 复制代码
let ret;
let s1: &str = "hello";
'x {
    'block {
        let s2: &str = "world!";
        'y {
            ret = before_str(s1, s2);
            println!("{}", ret);
        }
    }
    println!("{}", ret);
}

同样的,s1s2的生命周期分别是'x'y。调用before_str后,'a = 'x'b = 'y,最后返回值生命周期为'a。因此ret的生命周期是'a,也就是ret的生命周期是'x

两次println!都在'x的生命周期范围内,当然代码编译通过,正常执行。

当生命周期参数出现在函数签名中,大部分都是以上这种情况(后面有例外),即通过参数来确定一个生命周期,最后通过生命周期来约束返回值的生命周期。这不会修改任何运行逻辑,只是让编译器在函数调用后,拿到返回值时可以分析出参数与返回值的生命周期关系,好进行借用检查。

如果函数返回了一个借用,只有可能是两种情况:

  1. 来自函数参数的借用
  2. 函数体内部变量的借用

Rust不允许返回函数体内部变量的借用,这一定会造成垂悬引用('static除外)。此处的'static生命周期会在后续讲解。

如果一个函数在参数没有借用情况下返回了借用,那么一定是一个垂悬引用。

rust 复制代码
fn make_borrow<'a>() -> &'a i32 {
    let i = 10;
    &i
}

此处返回的&i32就指向了函数内部的临时变量,这会导致函数外部拿到一个垂悬借用,一旦访问就会报错,因此Rust不允许。

Rust是通过生命周期来保证的,如果函数返回借用,那么该借用的生命周期必须是来自函数参数的生命周期,或者'static生命周期。

比如以上函数中,返回的'a生命周期就不来自函数参数,因此编译无法通过。


复合类型

在结构体中,也可以使用借用作为字段,但是必须进行生命周期标注。

例如:

rust 复制代码
struct Borrow<'a> {
    x: &'a i32,
    y: &'a i32,
    z: &'a i32,
}

语法和之前一致,生命周期声明在<>内部,借用&必须写明生命周期参数。

同样的,'a这个生命周期参数要小于等于所有字段的生命周期,因此'a是三个字段生命周期的最小值。

当生命周期作用于结构体时,往往限制的是结构体本身的生命周期:结构体实例的生命周期必须不长于它的所有借用。

例如:

rust 复制代码
fn main() {
    let b;
    {
        let x = 10;
        let y = 20;
        let z = 30;

        b = Borrow {
            x: &x,
            y: &y,
            z: &z,
        };
    }
    println!("x: {}, y: {}, z: {}", b.x, b.y, b.z);
}

此处结构体b的生命周期比xyz的生命周期都更长,等到三个变量销毁,b内部的借用就是垂悬引用了。编译器根本不允许这段代码通过,因为它违反了生命周期。

对以上代码进行生命周期标注:

rust 复制代码
fn main() {
    let b;
    'block {
        let x = 10;
        'x {
            let y = 20;
            'y {
                let z = 30;
                'z {
                    b = Borrow {
                        x: &x,
                        y: &y,
                        z: &z,
                    };
                }
            }
        }
    }
    println!("x: {}, y: {}, z: {}", b.x, b.y, b.z);
}

结构体初始化时,传入了三个生命周期'x'y'z。最后'a = min('x, 'y, 'z),也就是'a = 'z,因此b本身的生命周期不长于'z

而在'z之外,使用println!输出了b,这就违背了生命周期的要求,所以编译器报错。

除了结构体,枚举体、元组结构体也可以携带借用,都要进行显式生命周期标注,它们的功能和结构体类型:复合类型的生命周期不能超过它所有借用的生命周期。


方法

当给复合类型实现方法时,需要在impl后声明泛型参数,包括生命周期参数。

例如:

rust 复制代码
struct Borrow<'a> {
    s: &'a str,
}

impl<'a> Borrow<'a> {
    fn new(s: &'a str) -> Borrow<'a> {
        Borrow { s }
    }
}

此处Borrow自带一个'a生命周期参数,那么impl的时候必须声明这个'a

后续在所有方法中,可以自由使用已经声明的生命周期参数,当然方法也可以自己在函数签名中声明新的生命周期参数。

接下来看一个案例:

rust 复制代码
struct Borrow<'a> {
    s: &'a str,
}

impl<'a> Borrow<'a> {
    fn new(s: &'a str) -> Borrow<'a> {
        Borrow { s }
    }

    fn longer<'b>(&self, other: &'b Self) -> &'a str {
        if self.s.len() > other.s.len() {
            self.s
        } else {
            other.s
        }
    }
}

重点放在 longer 这个方法上,这段代码可以编译通过吗?

这段代码可以通过,没有问题。有人就要疑问了,other的生命周期是'b,而返回的生命周期是'a,你怎么保证'b的生命周期一定长于'a,为什么可以返回other.s呢?

比如这段代码就会报错:

rust 复制代码
fn longer_str<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

它会报错:Lifetime may not live long enough,也就是'b的生命周期可能没返回值'a长。

是这样没错,但是这两个不是同一个场景。

针对longer方法,要做出两点澄清:

  1. other.s的生命周期是'a,而不是'b
  2. 'b的生命周期小于等于'a

首先在longer这个方法中,Self的类型是Borrow<'a>,它已经带了生命周期参数,因此other: &Self本身就表示other里面的字符串s是满足'a生命周期的。所以返回值other.sself.x生命周期都是'a,根本没有违反规则。

其次,other: &'b Self中,'b是修饰& Self这个借用的,它表达的是整个Borrow<'a>整体的生命周期,而不是内部str的生命周期。而结构体的生命周期一定小于等于内部所有借用的生命周期,所以'b <= 'a

如果不好理解你尝试把函数签名改成这样试试看:

rust 复制代码
fn longer<'b>(&self, other: &'b Borrow<'a>) -> &'a str ;

参数other内含'a'b两个生命周期参数。

讲解这个案例并非要为难大家,而是强调在impl的时候,Self的类型是要带着泛型一起看的,比如说Box<i32>Box<f64>就不是一种类型,同样的这里的Self是自带生命周期'a的。


Trait

Trait中也可以使用泛型,自然也就可以使用生命周期参数。

Trait中的生命周期参数往往来源于实现它的具体类型,当某个方法要使用类型内部的生命周期参数,那么就需要在Trait中注明。

例如:

rust 复制代码
trait Longer<'l> {
    fn longer_str(&self) -> &'l str;
}

struct Borrow<'b> {
    x: &'b str,
    y: &'b str,
}

impl<'a> Longer<'a> for Borrow<'a> {
    fn longer_str(&self) -> &'a str {
        if self.x.len() > self.y.len() {
            self.x
        } else { 
            self.y
        }
    }
}

这段代码要分三层看:

  • Trait层:此处在Longer这个Trait声明了生命周期'l,在函数longer_str的返回值中返回了带有该生命周期的借用。Trait 自身不知道这个 'l 具体是谁,只是声明"返回值的生命期不会比 'l 更长"。

  • 结构体层:在 Borrow<'b> 中,两个字段 xy 都借用了 'b 生命周期的数据,也限制了结构体实例的生命期不能长于 'b

  • impl实现层:在 impl<'a> Longer<'a> for Borrow<'a> 这一行,'aTrait'l 与结构体的 'b 统一成了同一个生命周期。因此 longer_str 返回的引用 'a 来自结构体内部两段 'a 借用之一。

我特意把三处生命周期用三个字母区分开了,主要是强调在impl之前,'b'l是两个毫不相干的生命周期。

你可以参考以下泛型代码来理解:

rust 复制代码
struct Pair<T, U> {
    first: T,
    second: U,
}

fn same_type_pair<M>(first: M, second: M) -> Pair<M, M> {
    Pair { first, second }
}

在结构体Pair中,它接收两个泛型参数TU,在same_type_pair中要获取两个元素类型相同的Pair,于是用一个M泛型同时作用于TU。这是一个用泛型统一泛型的过程,原本不确定TU的具体类型,到了same_type_pair中,虽然还是不能确定楚TU最后是谁,但是可以知道的是TU最后是同一个泛型M

同理,'l'b原本是两个不相关的生命周期,它们各种遵守各自的规则,等到最后调用的时候才会决定'l'b的具体生命周期。最后'l去限制返回值,'b限制结构体的实例。在impl中则把它们用'a统一了起来,它既要去限制结构体实例,又要限制返回值生命周期,而这个融合过程的结果就是:方法返回值的生命周期来自于结构体内部。

我之前说,Trait中的生命周期参数往往来源于实现它的具体类型,这只是一个结果。而导致它的是多个生命周期之间通过impl结合的过程。


省略规则

并不是所有的情况下都要显式地注明生命周期,在一些简单的场景下可以通过生命周期省略规则,来省略生命周期。

对于函数来说,参数中的生命周期叫做输入生命周期,而返回值的生命周期叫做输出生命周期。

规则如下:

  1. 对没有生命周期的函数参数,自动分配互不相同的生命周期参数
  2. 如果只有一个输入生命周期,该生命周期自动分配给输出生命周期
  3. 如果存在多个生命周期,且第一个参数为&self或者&mut self,则将self的生命周期分配给输出生命周期

一条一条规则解析。

第一条规则:没有生命周期的函数参数,自动分配互不相同的生命周期。

它可以自动为所有的借用分配生命周期。

例如:

rust 复制代码
// 源代码:
fn longer_str(x: &str, y: &str)

// 补全后:
fn longer_str<'a, 'b>(x: &'a str, y: &'b str)

此处的longer_str有两个借用xy,用户没有为其声明任何生命周期参数。编译器拿到这个函数签名后,自动为xy声明不同的生命周期'a'b

而且这条规则也作用于已经声明部分生命周期的情况。

例如:

rust 复制代码
// 源代码:
fn multiple_borrow<'a>(x: &'a str, y: &str, m: &str, n: &str)

// 补全后:
fn multiple_borrow<'a, 'b, 'c, 'd>(x: &'a str, y: &'b str, m: &'c str, n: &'d str)

在源代码中,只为x注明了一个生命周期'a,剩下三个参数都没有注明生命周期,于是编译器自动为后面三个参数注明了不同的生命周期。

这条特性可以让我们只标注出与返回值有关的生命周期,其它的借用全部省略生命周期,让编译器自己补。

第二条规则:如果只有一个输入生命周期,该生命周期自动分配给输出生命周期。

因为输出生命周期只可能来自于函数参数或者函数内部变量,而内部变量往往会造成垂悬引用,所以只有一个输入生命周期的时候,直接将其分配给输出生命周期,往往是正确的。

这里还要额外说明的是:三条规则是按顺序依次执行的。

例如:

rust 复制代码
// 源代码:
fn do_something(x: &mut i32) -> &mut i32

// 规则一:
fn do_something<'a>(x: &'a mut i32) -> &mut i32 

// 规则二:
fn do_something<'a>(x: &'a mut i32) -> &'a mut i32

对于do_something的源代码,输入一个借用,输出一个借用。先执行规则一,为x补上一个生命周期。

补完生命周期后,发现此处仅有x一个参数,符合规则二的要求,于是自动为返回值分配'a生命周期。

你甚至可以一开始就写:

rust 复制代码
fn do_something<'a>(x: &'a mut i32) -> &mut i32 

这些编译器也不会报错的,因为它会跳过规则一,执行规则二后会把返回值的生命周期补全。这样写就有点不明所以,但是它可以跑,因为你自己声明的生命周期也可以被规则二拿去补返回值的生命周期。

但是如果经过规则一后,发现函数中不止一个输入生命周期,此时就不会执行规则二。

规则三: 如果存在多个生命周期,且第一个参数为&self或者&mut self,则将self的生命周期分配给输出生命周期。

如果第一个参数为&self,那么说明这是一个方法,在方法中往往是针对类型本身操作,因此返回的借用更可能是类型本身内部的借用,所有多个参数情况下,有&self的生命周期优先使用。

例如:

rust 复制代码
struct Example<'e> {
    data: &'e str,
}

impl<'e> Example<'e> {
    fn get_data(&self, _other: &str) -> &str {
        self.data
    }
}

此处的get_data没有注明生命周期,经过编译器补全,函数签名如下:

rust 复制代码
// 源代码:
fn get_data(&self, _other: &str) -> &str

// 规则一:
fn get_data<'a, 'b>(&'a self, _other: &'b str) -> &str

// 规则三:
fn get_data<'a, 'b>(&'a self, _other: &'b str) -> &'a str

经过规则一,为所有参数补全生命周期。经过规则二,由于不止一个输入生命周期,因此没有变化。

规则三优先将'a分配给输出参数,此时整个生命周期补全。

当然这个生命周期规则不是万能的,它是只可以帮助你在部分场景下省略一些生命周期,比如说:

  • 只有一个输入生命周期:可以省略所有生命周期
  • 不是所有输入生命周期都和输出生命周期有关:可以省略和返回值无关的输入生命周期
  • 输出生命周期用和self相同的:可以省略所有生命周期

如果经过三个规则补充后,仍有生命周期没有确定,那么编译器会报错。


借用延长生命周期

之前所有权的博客讲解了表达式与上下文,它们都被分为值和位置两种类型。值表达式放到值上下文,则拿到一个值。位置表达式放到位置上下文,则得到一个地址。位置表达式放到值上下位,则可能移动或拷贝。

还有一种情况,那就是值表达式放到位置上下文,在Rust中这是允许的,这就是借用临时值。

Rust中,允许直接对一个值表达式进行借用,此时相当于创建一个临时变量,再对这个临时变量进行借用,从而将值的生命周期延长到与借用一致。

例如:

rust 复制代码
fn main() {
    let b = &10;
}

此处对一个字面量10进行了借用,它等效于:

rust 复制代码
fn main() {
    let tmp = 10;
    let b = &tmp;
}

先将值赋值到一个临时变量中,随后再对临时变量进行借用。

要注意的是,这个临时变量的创建位置,是在最终的借用者的前一行创建的,例如以下代码:

rust 复制代码
fn main() {
    let b = {
        &String::from("hello");
    };
}

这段代码是合法的,有人可能疑惑,String的生命周期不应该被限制在{ }内部吗,为什么这个借用可以突破生命周期?

实际上它等效于:

rust 复制代码
fn main() {
    let tmp = String::from("hello");
    let b = {
        &tmp;
    };
}

此处的最终借用者是b,所以临时变量不是创建在{ }内部,而是在b的前一行。

基于这个特性,你可以写出这样的代码:

rust 复制代码
fn main() {
    let x = 10;
    
    let b = if x > 10 {
        &String::from("hello")
    } else {
        &String::from("world")
    };
}

当然你也可以进行可变借用:

rust 复制代码
fn main() {
    let b = &mut 10;
}

它等效于:

rust 复制代码
fn main() {
    let mut tmp = 10;
    let b = &mut tmp;
}

当借用一个字面量的时候,如果是不可变借用,不会创建临时变量,而是直接指向字面量本身。

例如:

rust 复制代码
fn main() {
    let x = &10;
    let y = &10;

    println!("{:p}", x);
    println!("{:p}", y);
}

此处的xy都是对字面量10进行了不可变借用,随后输出xy指向的地址。如果你尝试运行代码,会发现xy的地址居然相同。因为编译器已经确定你不会修改这个值,于是直接指向已经存在的字面量,减少拷贝来优化效率。

匿名生命周期

由于结构体的生命周期不能省略,很多时候就会写出不必要的生命周期。

例如:

rust 复制代码
struct Foo<'a>(&'a str);

impl<'a> Foo<'a> {
    fn get(&self) -> &'a str {
        self.0
    }
}

此处Foo内部有'a生命周期,因此impl就要声明它,导致impl内出现了三次'a。但是其实它非常没必要,因为get就一个输入,编译期实际上自己就可以把生命周期推出来。

为此,Rust 2018引入了匿名生命周期'_,它可以省略一些无意义的生命周期名称。

匿名生命周期的行为分两类:

  • 输入位置 :每个 '_ 都会生成一个 新的独立生命周期。
rust 复制代码
fn bar(x: &'_ str, y: &'_ str) -> bool {
    x == y
}

这里的两个 '_ 是不同的生命周期,等效于'a'b的写法。

  • 输出位置'_ 会按照省略规则自动推导,如果无法推导则报错
rust 复制代码
fn foo(x: &str) -> &'_ str { x }

因为只有一个参数,这里的'_生命周期和x相同。

有人可能有疑问,上面两个例子不写'_也能用省略规则推导出来,为什么还要这种语法?

答案在于,'_也算一种内置的生命周期,它无需声明即可使用。

  1. 在方法实现中,为实现的类型省略
rust 复制代码
struct Pair<'a, 'b>(&'a str, &'b str);

impl Pair<'_, '_> {
    fn both_len(&self) -> (usize, usize) {
        (self.0.len(), self.1.len())
    }
}

此处的Pair是一个带有生命周期的结构体元组,如果直接进行impl,签名就要写成impl<'a, 'b> Pair<'a, 'b> {}。但是在方法both_len中,返回值没有借用,完全可以让省略规则自己推。在前面声明的两个生命周期完全没有用,而且还把签名变得很长。

此时就很适合用'_,表示Pair内部的生命周期让编译器自己用省略规则去推断,在方法中用户不会对这些生命周期做操作。

由于它不用声明,所以impl后面就不用写<>了,只需要在Pair中进行,缩短了签名。

  1. Trait生命周期省略
rust 复制代码
trait Show<'a> {
    fn show(&self) -> &'a str;
}

struct S<'a>(&'a str);

impl Show<'_> for S<'_> {
    fn show(&self) -> &str { self.0 }
}

这个案例和第一个案例是相同的。在show方法中,由于只有一个self参数,编译器完全可以自己推出返回值的生命周期。但是讨厌的impl必须<>声明生命周期,最后就可能变成impl<'a, 'b> Show<'a> for S<'b>,但是这两个生命周期在方法中根本没有用上。

因此使用'_无需声明的特性,省略了'a'b的声明,缩短了签名。

  1. 在函数参数中,为内部借用省略
rust 复制代码
struct Sender<'a>(&'a str);

struct Buffer<'b> {
    target: &'b mut String,
}

impl Sender<'_> {
    fn send_to(&self, b: &mut Buffer<'_>) {
        b.target.push_str(self.0);
    }
}

以上代码有两个结构体,Sender是发送方,内含一个借用,Buffer是接收方,内部也含一个借用。

Sender实现send_to方法的时候,第二个参数是Buffer,由于内部有借用,所以声明了一个生命周期。但是这个生命周期与当前函数无关,当前函数只是拿到target然后插入一段字符串,没有涉及到借用的生命周期问题。

因此使用Buffer<'_>,表示Buffer内部有一个借用,但是我不在乎它是什么,编译器会自动推断它最后的生命周期。

实际上,你就算不写生命周期,直接b: &mut Buffer也是可以的。在'_出现之后,Rust建议显式地写出结构体内部的生命周期,即使不使用也声明为'_表示它内部是存在借用的。

  1. 在返回值中,为内部借用省略
rust 复制代码
struct Wrap<'a>(&'a str);

fn wrap(s: &str) -> Wrap<'_> {
    Wrap(s)
}

此处返回了一个结构体元组,同样是包含了内部的借用,函数返回了这个结构体。

由于wrap函数只有一个参数,其实返回值的生命周期可以直接用省略规则推断出来。因此返回值使用匿名生命周期,Wrap<'_>表示返回的结构体内部有一个借用。

同样的,你也可以不写出这个借用,直接-> Wrap。编译器可以编译通过,但是会有警告,表示这里有一个隐藏的生命周期,建议你显式地写出来。

以上四个场景,实际上就是匿名生命周期的两个主要运用场景:

  • impl时,省略不关心的生命周期的省略,从而缩短impl的签名
    • 案例一:省略实现方法的类型的生命周期
    • 案例二:省略实现的Trait的生命周期
  • 在结构体中有借用,且这个借用的生命周期编译器可以自己推的场景,显式注明内部有一个借用,从而规范代码,消除警告
    • 案例三:注明函数参数的内部借用
    • 案例四:注明返回值的内部借用


静态生命周期

已经声明的生命周期是可以直接在借用时使用的。

比如:

rust 复制代码
fn longer_str<'a>(x: &'a str, y: &'a str) -> &'a str {
    let left: &'a str = x;
    let right: &'a str = y;
    
    if left.len() > right.len() {
        left
    } else { 
        right
    }
}

在函数签名中已经声明了'a这个生命周期,在函数体创建变量的时候就可以用'a生命周期创建借用。只是这个特性很少用,因为在同一个函数内部,生命周期是可以让编译器自己推断的,不需要我们自己去指定。而且该函数中leftright创建确实非常多此一举,直接用xy做比较就好。

Rust内置了一种特殊的生命周期,叫做静态生命周期'static。持有这种生命周期的值,说明它和整个程序活的一样久,它也是所有生命周期的最大值。生命周期'static不用声明就可以直接使用。

所有字面量都自带'static生命周期,不论是字符串还是数值。

rust 复制代码
let s: &'static str = "hello";
let i: &'static i32 = &10;

以上代码中,在&标注了'static生命周期,是合法的。因为字面量"hello"10本身就是在整个程序运行期间都存活的,它们被存储在堆栈以外的位置,比如"hello"会在常量区。

这里进行了&10的借用,对字面量进行借用的时候,如果不声明生命周期,那么生命周期和借用一样长,这是刚才说过的。如果声明'static,那么生命周期就是'static

使用staticconst关键字定义的量,也是'static生命周期。

rust 复制代码
const YEAR: i32 = 2025;
let year: &'static i32 = &YEAR;

static MONTH: i32 = 10;
let month: &'static i32 = &MONTH;

以上代码分别创建了YEARMONTH'static借用,这些都是合法的。

函数可以直接返回'static生命周期的借用,即使它不来自于参数中的生命周期。

rust 复制代码
fn get_hello() -> &'static str {
    "hello"
}

以上代码返回了一个的借用,因为是'static所以可以直接返回。

案例:

rust 复制代码
fn longer_str<'a> (x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let ret;
    {
        let s1: &'static str = "hello";
        let s2: &'static str = "hello";
        ret = longer_str(s1, s2);
    }
    println!("{}", ret);
}

这里的 longer_str就是最初见到的那个函数,它没有任何变化。

不同的是传入的两个参数生命周期都是'staticret接受到返回值后,可以在s1s2的作用域外部访问。

将生命周期标注出来:

rust 复制代码
'static {
    fn main() {
        let ret;
        'block {
            let s1: &'static str = "hello";
            let s2: &'static str = "hello";
            ret = longer_str(s1, s2);
        }
        println!("{}", ret);
    }
}

'static生命周期是整个程序的运行期间存活,它包括了整个main函数从开始到结束的过程。

传入的s1s2生命周期都是'static,因此'a = min('static, 'static) = 'static,因此ret的生命周期就是'static。最后println!也确实是在'static生命周期内部使用的,因此合法。

这里也体现了,最后生命周期具体是什么是由调用者决定的,同样的longer_str函数在不同情况下,'a的生命周期也不尽相同。

最后一点:长的生命周期可以收缩为短的生命周期,因此'static可以收缩成任意生命周期。

例如:

rust 复制代码
fn bigger<'a>(x: &'a u32, y: &'a u32) -> &'a u32 {
    if x == y {
        let ret: &'a u32 = &0;
        ret
    } else if x > y {
        x
    } else {
        y
    }
}

这是一个输出两个数值的较大值的函数,如果数值相等就输出0

由于返回的是一个借用,因此相等时返回的是&0,它本身是'static生命周期,但是let ret: &'a u32 = &0;强行让它收缩成了'a生命周期,这是合法的。

这里只是显式地展示生命周期的收缩过程,更好的写法是直接返回&0

rust 复制代码
if x == y {
	&0
} else ...

存活约束 outlives bound

在泛型<>中,可以使用泛型约束,那么生命周期是否可以进行约束呢?

Rust提供了生命周期约束的语法,一共有两种,一种是生命周期之间的约束,另一种是对类型的生命周期约束。

生命周期约束

第一种约束是生命周期之间的约束,它描述的是生命周期之间一对一的包含关系。

语法:

rust 复制代码
<'a, 'b: 'a>

或者:

rust 复制代码
where
    'b: 'a,

这语法既可以写在<>内部,也可以写在where后面。'b: 'a表示'b的生命周期一定长于'a的生命周期。

例如:

rust 复制代码
fn borrow_chain<'a, 'b: 'a>(x: &'b i32) -> &'a i32 {
    x
}

此处'b: 'a限定了两个生命周期的关系,要求返回一个'a生命周期,但是返回的x'b生命周期。但是'b: 'a已经确定了'b一定长于'a,那么返回的时候'b就可以收缩为'a

你可以理解为以下等效代码:

rust 复制代码
fn borrow_chain<'a, 'b: 'a>(x: &'b i32) -> &'a i32 {
    let ret: &'a i32 = x;
    ret
}

但是这个语法只能描述一对一的关系,不支持用'c: 'a + 'b来表达'c同时长于两个生命周期。

你只能这么写:<'a, 'b: 'a, 'c: 'b>,这表示'a < 'b < 'c


类型约束

生命周期也可以用于约束一个类型,表示某个类型内部的所有借用都必须长于指定生命周期。

语法:

rust 复制代码
<T: 'a + 'b ...>

或者:

rust 复制代码
where
    T: 'a + 'b ...,

它同时支持<>where语法。T: 'a表示类型T内部的所有借用,生命周期都要长于'a。如果有多个生命周期,可以用+间隔,表示长于列出的所有生命周期。

rust 复制代码
struct Ref<'a, T: 'a> {
    r: &'a T
}

因为 r 借用了 T,因此 r 的生命周期 'a 必须要比 T 的生命周期更短。

Rust 1.30 之前,必须显式指明T的生命周期。 1.31 版本后,编译器可以自动推导 T: 'a,就可以省略这个约束:

rust 复制代码
struct Ref<'a, T> {
    r: &'a T
}

因为如果可以得到&'a T借用,那么说明T内部的生命周期一定长于'a,不需要你显式指定T: 'a


Trait 对象的生命周期

如果Trait对象的具体实现类型带有生命周期参数,那么Trait对象本身的生命周期如何处理?

不知你还是否记得dyn是可以使用约束的,比如:

rust 复制代码
dyn Foo + Debug

这表示这是一个FooTrait对象,但是要求改对象的具体类型还要实现Debug

Trait对象也可以使用这个语法,来限制生命周期。

例如:

rust 复制代码
Box<dyn Foo + Debug + 'static>

就表示这个Trait对象的生命周期是'static,你可以通过这种语法显式地指明生命周期。

Trait对象有特殊的生命周期规则:

  1. Trait对象默认是'static生命周期
  2. 如果实现Trait的类型只含一个'a生命周期,那么Trait对象的生命周期就是'a
  3. 如果实现Trait的类型中有多个生命周期,则需要明确指定生命周期

第一点:Trait对象默认是'static生命周期。

例如:

rust 复制代码
trait Foo {
    fn show(&self);
}

struct Bar; // 没引用字段

impl Foo for Bar {
    fn show(&self) {
        println!("I am Bar: I live forever ('static).");
    }
}

fn main() {
    let boxed: Box<dyn Foo> = Box::new(Bar);
    // 实际等同于 Box<dyn Foo + 'static>
    boxed.show();
}

let boxed: Box<dyn Foo> = Box::new(Bar);,就是用Bar创建了一个Trait对象。

它的类型看起来是Box<dyn Foo>,但这是省略了生命周期的版本。此处Bar内部不含任何借用字段,自然也就没有生命周期的要求。因此它的生命周期默认为'static,等效于Box<dyn Foo + 'static>

第二点:如果实现Trait的类型只含一个'a生命周期,那么Trait对象的生命周期就是'a

例如:

rust 复制代码
trait Foo {
    fn value(&self) -> i32;
}

struct Bar<'a> {
    r: &'a i32,
}

impl<'a> Foo for Bar<'a> {
    fn value(&self) -> i32 {
        *self.r
    }
}

fn make<'a>(num: &'a i32) -> Box<dyn Foo + 'a> {
    Box::new(Bar { r: num })
}

fn main() {
    let x = 42;
    let foo = make(&x); // 生命周期随 &x
    println!("value = {}", foo.value());
}

这个案例中,Bar带了一个生命周期参数'a,在make中返回一个Box<dyn Foo>。由于实现该Trait对象的实例携带了唯一的生命周期'a,因此对象的生命周期也是'a,最后返回类型是 Box<dyn Foo + 'a>

要注意的是,不能直接返回Box<dyn Foo>,因为它默认等于Box<dyn Foo + 'static>, 而'a不满足'static,编译无法通过。

第三点:如果实现Trait的类型中有多个生命周期,则需要明确指定生命周期

例如:

rust 复制代码
trait Foo {
    fn pair(&self) -> (i32, i32);
}

struct Bar<'a, 'b> {
    x: &'a i32,
    y: &'b i32,
}

impl<'a, 'b> Foo for Bar<'a, 'b> {
    fn pair(&self) -> (i32, i32) { (*self.x, *self.y) }
}

fn make<'a, 'b: 'a>(x: &'a i32, y: &'b i32) -> Box<dyn Foo + 'a> {
    Box::new(Bar { x, y })
}

fn main() {
    let a = 10;
    let b = 20;
    let foo = make(&a, &b);
    println!("{:?}", foo.pair());
}

此处Bar内含两个生命周期'a'b。在make函数中,返回了对象Box<dyn Foo>,实现者是Bar

由于Bar内部带有两个生命周期,必须指明具体的生命周期,此处指明为'a。同样的,如果不指明那么默认返回'static,不论'a还是'b都无法满足。

另外的,由于返回值中同时使用了'a'b的生命周期,而指定了返回'a生命周期。Rust无法保证'b生命周期一定大于'a生命周期,因此在前面加上约束'b: 'a


HRTB 高阶生命周期

在部分场景,比如在impl 内部带有泛型,而当这个泛型又被实例化为借用,就会导致编译期无法进行更加细致的推断。

示例:

rust 复制代码
trait DoSomething<T> {
    fn do_sth(&self, value: T);
}

impl<'a, T: Debug> DoSomething<T> for &'a usize {
    fn do_sth(&self, value: T) {
        println!("{:?}", value);
    }
}

fn foo<'f>(b: Box<dyn DoSomething<&'f i32>>) {
    let s: i32 = 10;
    b.do_sth(&s)
}

fn main() {
    let x: Box::<&usize> = Box::new(&2);
    foo(x);
}

首先有一个泛型Trait,内含一个do_sth方法,这个方法接受一个泛型value: T

随后为&usize实现了这个方法,并在impl标注了生命周期'a

foo方法中接受一个Trait对象,并显式指定T = &i32,标注了借用的生命周期为'f。在foo中,调用了Trait对象的do_sth方法,并把内部变量&s作为value传入。

最后在main中创建一个x,它是Box<&usize>,再把它作为Trait对象传入。

从逻辑上来说,它是很安全的代码。在impl中,value参数的生命周期和&'a self是毫不相干的,在内部直接输出了value就返回了。但是编译器并不这么认为,最后报错s的生命周期不够长。

假设 &2&'u 2,也就是生命周期为'u。拿Box::<&usize>作为参数传入Box<dyn DoSomething<&'f i32>>,最后根据前面说的推导规则,最后b就是Box<dyn DoSomething<&'f i32> + 'u>,即整个Trait对象的生命周期为'u

问题来了,作为do_sth&self参数,'a生命周期最后就被约束为了'u,那么'f最后具体的生命周期是什么?

这要从impl的签名切入:

rust 复制代码
impl<'a, T: Debug> DoSomething<T> for &'a usize

此处T是一个泛型,它没有任何针对生命周期的描述,因此外部的'f不被任何生命周期所约束。这也就意味着'f可以是任何生命周期,不限长短。

再看b的最终的类型描述:

rust 复制代码
Box<dyn DoSomething<&'f i32> + 'u>

这段代码的意思是,这是一个DoSomethingTrait对象,它本身的生命周期为'u。现在接受一个&i32的参数,且生命周期为'f

刚刚说了'f不受到任何约束,因此'f可以是任何生命周期,事实上代码逻辑也是不关心'f生命周期的。

但是对于Rust编译器来说,它必须给'f一个具体的生命周期,Rust并不能判断你的代码逻辑是什么样的,如果'f < 'u,那么代码内部就有可能因为'f生命周期不够长,导致内存错误。

因此:编译器保守地要求'f >= 'u,此时性质就发生了变化。

回到以下两段代码:

rust 复制代码
fn foo<'f>(b: Box<dyn DoSomething<&'f i32>>) {
	let s: i32 = 10;
	's {
		b.do_sth(&s)
	}
}

fn main() {
	let x: Box::<&usize> = Box::new(&2);
	'u {
		foo(x);
	}
}

'umain的生命周期,而'f最后实例化为's。很明显's >= 'u是不可能成立的,因此报错's生命周期不够长。

这就是Rust生命周期带来的麻烦,它无法表示一个"无关"的生命周期,从而使用一个"无关"的借用。

高阶生命周期HRTB就是专门处理这种情况的,语法如下:

rust 复制代码
for<'f> Outer<&'f inner>

这表示Outer的内部的'f是一个不受约束的生命周期,它可以接受任意生命周期。

因此以上代码就可以改成:

rust 复制代码
fn foo(b: Box<dyn for<'f> DoSomething<&'f i32>>) {
    let s: i32 = 10;
    b.do_sth(&s)
}

fn main() {
    let x: Box::<&usize> = Box::new(&2);
    foo(x);
}

此处把<'f>从函数泛型声明中删掉了,而是声明到了 DoSomething前面,表示DoSomething内部有一个生命周期'f,但是它不受到任何约束,它可以是任意生命周期。

b.do_sth(&s)的时候,只要保证s的生命周期可以活过这一行函数调用就行了。

而在do_std方法内部,它被单态化为了:

rust 复制代码
impl<'a> DoSomething<&i32> for &'a usize {
    fn do_sth(&self, value: &i32) {
        println!("{:?}", value);
    }
}

可以理解为,'f在方法内部已经不存在了,value相当于一个内部定义的临时变量,只保证它的生命周期可以活到这个函数结束。

示例:

rust 复制代码
struct Person<'a> {
    age: Option<&'a i32>,
}

impl<'a, 'f> DoSomething<&'f i32> for Person<'a> {
    fn do_sth(&mut self, value: &'f i32) {
        self.age = Some(value);
    }
}

fn foo(mut b: Box<dyn for<'f> DoSomething<&'f i32>>) {
    let s: &'static i32 = &10;
    b.do_sth(s);
}

fn main() {
    let mut p = Person { age: None };
    func(Box::new(p));
} 

以上代码是在之前基础上做的变化,实现do_sth的类型变成了Person。在do_sth内部,修改了self.age

这个例子中的传参过程和之前略有差别,main函数提供self,而foo使用函数内部的借用传到do_sth方法内部,而这个s的生命周期是'static

由于for<'f>使用了高阶生命周期,因此在do_sth内部可以视为'f已经是一个和外部无关的生命周期,只保证'f可以活到函数结束。而self的生命周期与main中的p相同,假设它的生命周期是'p,很明显'p >= 'f,因此self.age = Some(value)这行代码会报生命周期不够长的错误。

尽管s的生命周期是'static,经过for<>的擦除后,函数内部依然视为一个刚好活完这个函数的生命周期。

这种语法在后续闭包中会再次见到。


泛型思想

生命周期的语法放在<>中,并不是说符号不够用了,非要和泛型挤在一起,而是从逻辑上它们是类似的。生命周期就是一种特殊的泛型。

<>中,一共可以放三种类型的参数:

  • 类型参数T
  • 常量参数const N: usize
  • 生命周期参数'a

它们在<>声明时都没有具体的意义,比如说T只表示一个类型,你不知道类型是谁。而'a表示一个生命周期,但是这个生命周期还没有明确的范围。

比如泛型参数,对于一个函数想要表达未来会接收到某个类型,但是函数不知道调用者会传入什么类型。因此引入类型参数T来表达一个"未知但确定的类型",也就是说这里一定会有一个类型,但我现在不知道它是谁,等到调用者调用了函数,我就知道了,先放个T在这里占位。

同理,在编译时,Rust 的借用检查器在分析函数定义时,并不知道调用者会传入多大的生命周期。要想在定义时保持通用,就必须引入符号化参数来代表"某个未知但确定的生命周期"。因此用符号'a表示一个生命周期,不知道最后生命周期到底有多大,等到最后调用者传入了参数,只要把几个参数的生命周期取最小值,就知道最后生命周期是多大了。

比如之前的例子中:

rust 复制代码
let s1: &str = "hello";
'x {
    'block {
        let s2: &str = "world!";
        'y {
            ret = longer_str(s1, s2);
            println!("{}", ret); // success
        }
    }
    println!("{}", ret); // error
}

此处'x'y是具体的生命周期,当传入longer_str中,'a就有了具体的含义,这是一个由调用者决定生命周期参数的过程,也可以视为一种单态化。只是这个单态化不会生成多种函数版本,只是用于进行编译器的关系推断。

回看博客前文,我一直在强调是函数参数决定生命周期参数,而非让生命周期参数来约束函数参数,就是这个逻辑。就和调用者通过函数参数来决定泛型类型T,而非T去约束参数一样。即使确实有泛型约束的语法,但是不论怎么约束,最后还是由调用者决定最后的泛型是谁。

这种由泛型占位表示一个存在而不确定的属性,最后调用者决定泛型是谁的逻辑,就是Rust的泛型逻辑,它抽象化了一切可在编译期确定的属性。

<>中同时支持类型、生命周期、常量,是因为它们都遵循这个逻辑:在未知之前先声明,在使用之时再确定,而且在Rust源代码中它们也确实是使用相同的算法来处理的。


相关推荐
u0119608231 小时前
apscheduler
开发语言·python
李日灐1 小时前
C++STL: vector 简单使用,讲解
开发语言·c++
程序员-周李斌1 小时前
CopyOnWriteArrayList 源码分析
java·开发语言·哈希算法·散列表
Source.Liu1 小时前
【LibreCAD】 rs_vector.cpp 文件详解
rust·cad
晚风(●•σ )1 小时前
C++语言程序设计——【算法竞赛常用知识点】
开发语言·c++·算法
Byron Loong1 小时前
【C#】离线场景检测系统时间回拨
开发语言·c#
free-elcmacom1 小时前
机器学习入门<4>RBFN算法详解
开发语言·人工智能·python·算法·机器学习
韭菜钟1 小时前
在Qt中实现mqtt客户端
开发语言·qt
4***571 小时前
PHP进阶-在Ubuntu上搭建LAMP环境教程
开发语言·ubuntu·php