Rust能力养成系列之(38) 内存管理:生命周期深入

前言

如上篇所谈,这几篇文字对生命周期做些条分析里的接受,这里先从生命周期参数开始。

 

生命周期参数(lifetime parameter)

对于编译器不能通过检查代码来确定值生存期的情况,需要在代码中使用一些注释来告知Rust。为了与标识符( identifiers)区分,生命周期注释需要用到一个单引号',也就是在字母前加上'。因此,为了让之前的示例代码使用参数进行编译,我们在StructRef上添加一个生命周期注释,如下所示

// using_lifetimes.rs

struct SomeRef<'a, T> {
    part: &'a T
}

fn main() {
    let _a = SomeRef { part: &43 };
}

可见,生命周期由一个单引号 '表示,后面可以跟随任何有效标识符序列。按照惯例,Rust中使用的大多数生命周期使用'a、'b和'c作为参数。如果一个类型有多个生存周期,则可以使用更长的描述性生命周期名称,如'ctx、'reader、'writer,等等。这些参数在与泛型类型参数相同的位置以相同的方式进行声明。

在一些例子中,生存周期充当了一个通用参数,用于之后解析有效引用,但是也会存在一个具有具体值的生存周期。如下代码所示:

// static_lifetime.rs

fn main() {
    let _a: &'static str = "I live forever";
}

静态生存期意味着这些引用在程序的整个过程中都是有效的。Rust中的所有字面值字符串的生命周期都是'static ,而它们会进入已编译目标代码的数据段。

 

省略规则

只要在函数或类型定义中有引用,都涉及生命周期。大多数时候,开发者不需要使用显式的生命周期注释,因为编译器足够智能的进行推断,因为在编译时已经有很多关于引用的信息可用。比如,以下这两个函数签名是一样的:

fn func_one(x: &u8) → &u8 { .. }

fn func_two<'a>(x: &'a u8) → &'a u8 { .. }

在通常的情况下,编译器省略了func_one的生存周期的形参,不需要把它写成func_two的这种冗余形式。

当然编译器只能介绍在特定的情况下省略生命周期参数,这里存在省略的规则。在讨论这些规则之前,我们需要讨论一下输入和输出生命周期,这些仅在涉及接受引用的函数时会用到。

  • 输入生命周期(Input lifetime):作为引用的函数参数的生命周期注释被称为输入生命周期。
  • 输出生命周期(Output lifetime):对于作为引用的函数返回值的注释被称为输出生命周期。

需要注意的是,任何输出生命周期都源自输入生命周期。我们不能有一个独立于输入生命周期的输出生命周期,其只能是小于或等于输出生命周期。

省略生命周期,一般遵循以下规则:

  • 如果输入生命周期只包含一个引用,则预设输出生命周期是也是如此
  • 对于涉及self和&mut self的方法,输入生命周期会基于&self参数进行推断

但有时在不明确的情况下,编译器不会进行推断。考虑以下代码:

// explicit_lifetimes.rs

fn foo(a: &str, b: &str) -> &str {
    b
}

fn main() {
    let a = "Hello";
    let b = "World";
    let c = foo(a, b);
}

在using_lifetimes.rs代码中,RefItem存储了对任何类型T的引用。在本例中,返回值的生存期并不明确,因为涉及两个输入引用。可见,有时编译器并不能计算出引用的生存期,需要指定生存期参数。考虑现在的代码,自然不能编译

这是因为Rust无法计算出返回值的生命周期,需要开发者的帮助。

现在,总结一下,有很多地方必须指定生命周期,因为Rust编译器不能直接计算:

  • 函数签名(Function signatures)
  • 结构体和对应域(Structs and struct fields)
  • 实现块(impl blocks)

 

用户定义类型的生命周期

如果一个结构体定义中,拥有引用任何类型的字段,需要显式指定这些引用将存在多长时间。其语法类似于函数签名:首先在结构行上声明生存周期名称,然后在字段中进行使用。

以下是相关代码,语法极为简单:

//  lifetime_struct.rs

struct Number<'a> { 
    num: &'a u8 
}

fn main() {
    let _n = Number {num: &545}; 
}

这里意味着:Number的定义的生命周期与num的引用一样长。

 

impl block中的生命周期

当我们为带有引用的结构体创建impl块时,需要不止一次的重复生命周期声明和定义。例如实现之前所定义的struct Foo,相关语法将如下所示:

// lifetime_impls.rs

#[derive(Debug)]
struct Number<'a> { 
    num: &'a u8 
}

impl<'a> Number<'a> { 
    fn get_num(&self) -> &'a u8 { 
        self.num 
    }  
    fn set_num(&mut self, new_number: &'a u8) { 
        self.num = new_number 
    }
}

fn main() {
    let a = 10;
    let mut num = Number { num: &a };
    num.set_num(&23);
    println!("{:?}", num.get_num());
}

很繁冗吧,当然,在大多数情况下,生命周期是可以从类型本身推断出来的,之后可以省略带有<'_>语法的签名了。

 

多重生命周期(Multiple lifetimes)

就像泛型类型参数一样,如果有多个具有不同生存期的引用,应该指定多个生存周期参数。然而,在代码中处理多个生命周期时,还是有点麻烦的。大多数情况下,可以在结构体或函数中仅仅使用一次生命周期。但然而,在有些情况下,我们需要不止一个生命周期的注释说明。比如,我们正在构建一个解码器库,它可以根据模式和给定的已编码字节流解析二进制文件,其中有一个Decoder对象,它有一个对Schema对象的引用和一个对Reader类型的引用。相关代码如下:

// multiple_lifetimes.rs

struct Decoder<'a, 'b, S, R> {
    schema: &'a S,
    reader: &'b R
}

fn main() {}

在上面代码的定义中,当Schema是本地(local)时,有可能从网络获得Reader,因此它们在代码中的生存期可能不同。当开发者为这个解码器提供实现时,可以通过生命周期子类型(lifetime subtyping)来指定关系。

 

结语

生命周期还剩下两部分内容,放在下一篇来介绍,同时会开始指针内容的讨论。

 

主要参考和建议读者进一步阅读的文献

https://doc.rust-lang.org/book

深入浅出 Rust,2018,范长春

Rust编程之道,2019, 张汉东

The Complete Rust Programming Reference Guide,2019, Rahul Sharma,Vesa Kaihlavirta,Claus Matzinger

Hands-On Data Structures and Algorithms with Rust,2018,Claus Matzinger

Beginning Rust ,2018,Carlo Milanesi

Rust Cookbook,2017,Vigneshwer Dhinakaran

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值