前言
如上篇所谈,这几篇文字对生命周期做些条分析里的接受,这里先从生命周期参数开始。
生命周期参数(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