生命周期标注到底是什么?硬核Rust生命周期——看不懂这篇文章等于压根没学会生命周期

笔者至今没见过Rust的生命周期的优秀文档(至少中文圈和官方文档里面的教程基本不顶用)。最近通过实践终于豁然开朗,故分享笔记。其实核心思路是围绕着结构体生命周期标注来展开的;或者我暴论一下,函数的生命周期标注完全是为了服务结构体的生命周期标注的。

本篇没有生命周期的高级用法,实际上平时也基本不可能用到那些奇形怪状的东西。希望各位看完后都能理解为什么生命周期是标注,而不影响真实数据的内存管理。


更新日志

  • 2024.3.26:添加生命周期标注与签名的关系。
  • 2024.1.14:重新学习型变,增加大量分析,切换看问题的思路。更好地定义结构体生命周期标注。
  • 2024.1.13:初稿,比较潦草

初探

深入生命周期 - Rust语言圣经(Rust Course)

生命周期的目的是为了避免悬空引用。

编译器在编译时会列出各个变量生命周期的长度,保证不发生数据失效后的使用。但是有些情况无法在编译时得知生命周期:

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
//即使这样也会报错
fn longest(x: &str, y: &str) -> &str {
    x
}

编译器不看函数内部,而只看函数签名来推断返回值的生命周期。

静态生命周期

'static是特殊的生命周期,表示整个程序的运行时间。如所有字符串字面值都有此生命周期。

函数生命周期标注

<'a>为泛型生命周期参数。而生命周期标注(&'a str)仅仅用于描述关系,而不直接影响生命周期长度。

下面的代码给所有参数和返回值都进行了同一个标注,表示在该函数范围内它们的生命周期完全一样。从因果逻辑上来看,'a首先获得s1和s2的生命周期,并取交集,即取比较短的部分,然后规定返回值的生命周期必须在这部分里有效。

// 这是一个函数,它接受两个字符串切片,并返回一个字符串切片。
// 'a 是一个生命周期参数,它表示输入参数和返回值的生命周期必须“相同”。
fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

//正常的代码
fn main() {
    let string1 = String::from("long string is long");
    let result;
    let string2 = String::from("xyz");
    result = longest(string1.as_str(), string2.as_str());
    println!("The longest string is {}", result);
}

//会报错,因为result的生命周期取为string2,不能超出下一个花括号。
fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}

参数的交互、借用的时间也会受到生命周期标注的影响:

fn main(){
	let mut my_vec: Vec<&i32> = vec![];
	let val1 = 1;
	let val2 = 2;
	
	insert_value(&mut my_vec , &val1);
	insert_value(&mut my_vec , &val2);
}

//错误,因为push需要保证value要活的比my_vec久,但在此函数内无法推断
fn insert_value(my_vec: &mut Vec<& i32> , value: & i32){  
    my_vec.push(value);  
}

//错误,因为这会使得my_vec的可变引用被延迟到val1或val2被释放,导致多个可变引用同时存在
fn insert_value<'a>(my_vec: &'a mut Vec<&'a i32> , value: &'a i32){
	my_vec.push(value);
}

//正确,让Vec与其值的生命周期独立开,只要求值见的生命周期同步。当然也要求Vec的值比Vec活得久
fn insert_value<'r , 'val>(my_vec: &'r mut Vec<&'val i32> , value: &'val i32){
	my_vec.push(value);
}

//正确,无关的生命周期标注也可直接省略
fn insert_value<'val>(my_vec: &mut Vec<&'val i32> , value: &'val i32){
	my_vec.push(value);
}

生命周期自动推断

如果返回值的生命周期可以被推断,则可以省略。推断规则:

  1. 每一个是引用的参数都有它自己的生命周期参数
  2. 如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数
  3. 如果方法有多个输入生命周期参数并且其中一个参数是 &self 或 &mut self, 那么所有输出生命周期参数被赋予 self 的生命周期

struct生命周期标注

当struct里面存在需要标注生命周期的东西时(如引用类型的成员变量),要先在struct上以泛型的形式标注生命周期,如struct MyStruct<'a>。标注后,该引用变量的有效生命周期必须要长于struct标注的生命周期(而非struct本身!)。

由此我们可以得出struct的生命周期标注的两种描述:

  1. 表示了该struct中的所有引用的最短生命周期
  2. 表示了该struct的最久生存界限

理解起来很简单,结构体存活时间不能久到出现悬垂引用,因此创建结构体的时候指定生命周期标注以划分结构体本身的生命周期与其所有引用的生命周期

上面的结论在单个生命周期标注时完全成立,但在多生命周期标注时,其实也就和偏导函数一样各自独立看即可,只要每个标注都满足编译器要求即可成功编译。

struct标注的生命周期在创建其对象的时候确定下来,这也就帮助了该生命周期标注在多个引用和多个结构体之间进行交流协作。

标注了生命周期的struct必须在impl的时候在impl后面也标注一个,即impl<'a> MyStruct<'a>{...}

struct MyStruct<'a> {
    my_ref: &'a i32,
}

impl<'a> MyStruct<'a> {
    fn new(ref_to_i32: &'a i32) -> MyStruct<'a> {
        MyStruct { my_ref: ref_to_i32 }
    }

    fn print(&self) {
        println!("{}", self.my_ref);
    }
}

fn main() {
    let value = 5;
    let my_struct = MyStruct::new(&value);
    my_struct.print();
}

但如果impl里面并没有任何函数要用到生命周期标注的话,可以使用impl Mystruct<'_>{...}。其他推荐显式标注生命周期、但完全可以由编译器推导的情况下,也可以使用'_

普通变量继承生命周期(常见于泛型)

一个普通变量(而非引用)继承了一个生命周期,可以看做变量所标注的生命周期(MyStruct<'a>'a)继承于此生命周期,即要求变量里面的任何引用都不短于此生命周期。

注意,限制的只有变量内部的引用,而变量本身是可以提前消亡的。

如下面这段代码,当想要将一个泛型转为Box被存入Vector的时候,必须承诺在Box的生命周期期间该泛型内部所有引用保持合法。

trait MyTrait {  
    fn do_something(&self);  
}  
  
struct MyStruct<'a> {  
    items: Vec<Box<dyn MyTrait + 'a>>,  
}  
  
impl<'a> MyStruct<'a> {  
    //最暴力的方式是把a换成static,然后去掉其他a标注
    fn add<T: 'a + MyTrait>(&mut self, item: T) {  
        self.items.push(Box::new(item));  
    }  
}  
  
// 实现MyTrait的具体类型  
struct ConcreteType;  
impl MyTrait for ConcreteType {  
    fn do_something(&self) {  
        println!("Hello");
    }  
}  
  
fn main() {  
    let mut my_struct = MyStruct { items: Vec::new() };  
    my_struct.add(ConcreteType);  
    for item in &my_struct.items {  
        item.do_something();  
    }  
}

多个普通变量间的生命周期标注关系

MyStructBuilder里面存在受生命周期约束的变量items,build完成后该变量被转交给MyStruct,而builder本身消亡。此时就会要求items的生命周期标注不短于MyStruct的最短引用生命周期。于是可以在build返回MyStruct时让其生命周期也被标注为'a,这意味着告诉编译器:

  1. MyStruct的最短引用生命周期等于MyStructBuilder的最短引用生命周期(注意,引用生命周期是协变的,更长的生命周期也能塞进短的)。这很合理,毕竟MyStructMyStructBuilder生出来的,不应当贸然越界。
  2. items同时不短于MyStructMyStructBuilder的最短引用生命周期。这就使得其在两者内部可以完全自由使用。
trait MyTrait {  
    fn do_something(&self);  
}  

struct MyStruct<'a> {  
    items: Vec<Box<dyn MyTrait + 'a>>,  
}  
  
struct MyStructBuilder<'a> {  
    items: Vec<Box<dyn MyTrait + 'a>>,  
}  
  
impl<'a> MyStructBuilder<'a> {  
    fn new() -> Self {  
        MyStructBuilder { items: Vec::new() }  
    }  
  
    fn add<T: MyTrait + 'a>(&mut self, item: T) {  
        self.items.push(Box::new(item));  
    }  
  
    fn build(self) -> MyStruct<'a> {  
        MyStruct { items: self.items }  
    }  
}  
  
// 实现MyTrait的具体类型  
struct ConcreteType;  
impl MyTrait for ConcreteType {  
    fn do_something(&self) {  
        // 具体类型的实现  
    }  
}  
  
fn main() {  
    let mut builder = MyStructBuilder::new();  
  
    // 添加ConcreteType的实例到MyStruct  
    builder.add(ConcreteType);  
  
    // 构建ProStruct  
    let pro_struct = builder.build();  
  
    // 现在可以对ProStruct中的items调用do_something方法  
    for item in &pro_struct.items {  
        item.do_something();  
    }  
}

型变

Subtyping and Variance - The Rust Reference

Subtyping and Variance - The Rustonomicon

目的:让子可以完全满足父,或者说用父直接代替子,或者说把子直接当成父来用

若有一个类型构造器Foo(Class),以类型为参数来产生另一个类型。设A是B的父类。

  1. Foo(A)Foo(B)无关,则称为不变(invariant)
  2. Foo(A)Foo(B)父类,则称为协变(covariant)
  3. Foo(A)Foo(B)子类,则称为逆变(contra-variant)

原文:

  • F<T> is covariant over T if T being a subtype of U implies that F<T> is a subtype of F<U> (subtyping “passes through”)
  • F<T> is contravariant over T if T being a subtype of U implies that F<U> is a subtype of F<T>
  • F<T> is invariant over T otherwise (no subtyping relation can be derived)

不变实际上是要求A和B必须完全相同才能在转化后兼容。

对生命周期来说,A是B的子类意味着A长于B,即短生命周期的变量可以代理长生命周期变量。

Rust中,泛型T实际上是正经类型+生命周期的组合。因此泛型函数其实都会隐含生命周期标记。另外,下面表格的T也要看做带有生命周期标记a,然后按照前两个的规则进行整个的型变。

若A是B的父类,B是C的父类,则fn A => Cfn B => B的父类,因为参数为A的函数必然能处理参数B,而返回值为C的函数必然也能返回B(即fn A => C无论是入参还是返回值都可以直接在外部视为fn B => B)。因此fn类型对参数类型是逆变的,对返回值类型是协变的。

TypeVariance in 'aVariance in T
&'a Tcovariantcovariant
&'a mut Tcovariantinvariant
*const Tcovariant
*mut Tinvariant
[T] and [T; n]covariant
fn() -> Tcovariant
fn(T) -> ()contravariant
std::cell::UnsafeCell<T>invariant
std::marker::PhantomData<T>covariant
dyn Trait<T> + 'acovariantinvariant

可见只有函数参数是逆变的;涉及可变的类型(而非生命周期)基本都是不变;剩下都是协变。

如果T不变,但T形如TypeName<X>,则X也不变。

对结构体来说,其各个泛型的型变则独立地由其在每个成员变量中的型变所决定。比方说成员变量x的类型为fn(T) -> (),那么T在x中逆变。若T的所有出现都是协变,则T在结构体中协变;若都是逆变则是逆变;否则就是不变。

原文:A struct, informally speaking, inherits the variance of its fields. If a struct MyType has a generic argument A that is used in a field a, then MyType’s variance over A is exactly a’s variance over A.

However if A is used in multiple fields:

  • If all uses of A are covariant, then MyType is covariant over A
  • If all uses of A are contravariant, then MyType is contravariant over A
  • Otherwise, MyType is invariant over A
//成员变量类型决定泛型的型变
struct MyType<'a, 'b, A: 'a, B: 'b, C, D, E, F, G, H, In, Out, Mixed> {
    a: &'a A,     // covariant over 'a and A
    b: &'b mut B, // covariant over 'b and invariant over B

    c: *const C,  // covariant over C
    d: *mut D,    // invariant over D

    e: E,         // covariant over E
    f: Vec<F>,    // covariant over F
    g: Cell<G>,   // invariant over G

    h1: H,        // would also be covariant over H except...
    h2: Cell<H>,  // invariant over H, because invariance wins all conflicts

    i: fn(In) -> Out,       // contravariant over In, covariant over Out

    k1: fn(Mixed) -> usize, // would be contravariant over Mixed except..
    k2: Mixed,              // invariant over Mixed, because invariance wins all conflicts
}

struct Bar<'r>{
	_phantom: PhantomData<fn(&'r ())>, //逆变套协变=逆变
}
fn bar<'short, 'long: 'short>(
	mut short_bar: Bar<'short>,
	mut long_bar: Bar<'long>)
{
	//short_bar=long_bar; //编译不通过
	long_bar=short_bar; //编译通过
}

//如果两个参数都改成mut short_bar: &mut Bar<'short>,就会导致short_bar和long_bar互相都不能赋值

下面是一个比较典型的例子,使用Manager来包裹text数据,而Manager被存储在List中。外部不能直接访问Manager,而是让List使用Interface去包装Manager以提供接口。

//Manager和List都比较清晰,只规定自己管理的资源的生命周期下限。
struct Manager<'val>{  
    text: &'val str,
}

struct List<'val>{
	manager: Manager<'val>,
}

//好的写法,解耦使'r保持协变
//可以让Interface提前消亡(即不影响manager引用变量的消亡)
struct Interface<'r, 'val>{
	manager: &'r mut Manager<'val> //隐式定义'val: 'r
}

impl<'val> List<'val>{
	//解耦了:1.该方法对self的借用;2.self本身(普通变量)的生命周期
	//外部调用此方法后,在其借用self期间保证资源可用('r)
	//借用完成即业务完成,Interface和self可以一起消亡
	//但这一借用全过程不影响'val,即资源的生命周期
	//从协变角度看,返回值类型对r协变,因此函数对r协变,
	//或者说外界总可以用更短的r来接受Interface,是符合逻辑的
	pub fn get_interface<'r>(&'r mut self) -> Interface<'r, 'val> {
		Interface{
			manager: &mut self.manager,
		}
	}

	//也可以省略'r,让编译器自己推断
	//这里可以换一个视角看,Interface遵循比较正常的生命周期约定
	//借完就用,用完就同时释放借用,这种经典用法就可以依靠编译器推断
	//唯一需要限制的是Interface里面的val,要保证其资源可用
	pub fn get_interface_2(&mut self) -> Interface<'_, 'val> {  
	    Interface{  
	        manager: &mut self.manager,  
	    }  
	}
}

fn main(){  
    let mut list = List{  
        manager: Manager{  
            text: "abc",  
        }  
    };  
  
    let i1 = list.get_interface();  
    println!("1: {}",i1.manager.text);  
    let i2 = list.get_interface();  
    println!("2: {}",i2.manager.text);  
    
    //然而下面这个是过不了的,因为i1这个interface还没消亡,因此对list的借用没结束
	//let i1 = list.get_interface();  
    //let i2 = list.get_interface();
    //println!("1: {}",i1.manager.text);  
    //println!("2: {}",i2.manager.text);
}

正确的例子直接看没啥,但看看平时很可能写出的错误代码。下面的代码将Interface的两个生命周期标注合并为一个:

//错误的Interface写法
struct Interface<'a>
	manager: &'a mut Manager<'a>
}

这使得’a出现在成员变量中的两个地方,其中很显然的是&'a mut T对第一个a协变;虽然Manager对a协变(看Manager结构体,只有&'a一处,因此协变),但是由于&'a mut T对T不变,而第二个’a参与到了T中,使得&'a mut Manager<'a>对第二个a不变(协变+不变就是不变,很好理解,三种型变就和-1 0 1相乘运算一样可以嵌套推导)。因此根据规则,Interface对a不变。

这导致manager变量对a没有子类型。从生命周期逻辑上看,这使得Manager的生命周期标注与对Manager的引用生命周期强绑定。这显然是不合理的,Manager想活多久与对其引用持续多久之间只有继承关系,没有强绑定关系。但该分析并没有很直观的方法论,基本是抓瞎、凭感觉,真正的bug需要往后继续分析。

与错误的Interface配套的get_interface如下:

//配套错误的Interface写法
pub fn get_interface(&mut self) -> Interface<'val> {  
    Interface{  
        manager: &mut self.manager,  
    }  
}

编译器会推断出返回的Interface的隐式生命周期标注与self等同,又由于Interface对self不变,因此只能在self的生命周期为val的时候编译通过

实际上,给self标上val确实可以跑(难绷),但这就导致只能用一次get_interface,因为多次使用后,编译器不允许任何一个引用消亡,因为&mut T不变,所以若self引用消亡了的话,就说明List对象已经无法保证引用完全可用了。

另外,上面的错误代码将mut全部删去,使得a保持协变,那么代码是可以完全正常使用的。注意此时get_interface返回的Interface的生命周期是和self借用生命周期等同的,而manager指向的数据是val生命周期,被协变成self的生命周期。甚至给self和Interface都标上val也不会报错,毕竟都是只读借用,数据生命周期内借完不还也不会有事情。

//错误代码去掉mut后
struct Manager<'val> {  
    text: &'val str,  
}  
  
struct List<'val> {  
    manager: Manager<'val>,  
}  
  
struct Interface<'a> {  
    manager: &'a Manager<'a>,  
}  
  
impl<'val> List<'val> {  
    pub fn get_interface(&self) -> Interface {  
        Interface {  
            manager: &self.manager,  
        }  
    }  
}  
  
fn main() {  
    let mut list = List {  
        manager: Manager {  
            text: "abc",  
        }  
    };  
  
    let i1 = list.get_interface();  
    println!("1: {}", i1.manager.text);  
    let i2 = list.get_interface();  
    println!("2: {}", i2.manager.text);  
} 

生命周期标注与函数签名

生命周期标注往往使得函数或结构体定义不得不多写一些泛型参数,这实际上就是修改函数签名。生命周期标注的本质就是修改函数签名来进行各代码段见的关系推导。详见这篇文章:Rust:签名优先机制与生命周期标注——理解生命周期标注的真正意义

  • 46
    点赞
  • 48
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值