rust语言基础学习: 引用的生命周期
2020-07-11
前面学习了Rust的所有权规则、Move和Copy语义、引用和借用的知识,是时候学习Rust中关于引用有效性和生命周期的知识了。 在Rust中使用借用时必须遵循以下借用规则:
- 引用不能超过值的生命周期
- 在同一时刻一个值不能创建多个可变引用
- 在同一时刻一个值已经创建了不可变引用,不能再为其创建可变引用
借用规则的第一条引用不能超过值的生命周期,就是本节我们要学习的生命周期,Rust的编译器通过对比值和其引用的生命周期,来确保满足这条规则。
1.引用的生命周期 #
Rust中的每一个引用都有其生命周期(lifetime)。引用的生命周期指的是引用保持有效的作用域。
在有些情况下引用的生命周期是隐含的可以推断的,还有些情况是无法推断的,需要使用生命周期注解来标注引用的生命周期,这样能确保在运行时使用的引用总是有效。
1.1 隐式可推断的生命周期 #
Rust编译器中有一个借用检查器(borrow checker),通过比较作用域来确保所有引用都是有效的。例如下面的代码:
1{
2 let r; // ---------+-- 'a
3 // |
4 { // |
5 let x = 5; // -+-- 'b |
6 r = &x; // | |
7 } // -+ |
8 // |
9 println!("r: {}", r); // |
10} // ---------+
在这段代码的注释中,r
的生命周期为'a
,x
的生命周期为'b
,'b
比'a'
要小,则r
引用了一个比其生命周期小的值,所以程序会编译失败x does not live long enough
。Rust编译器保证了在Rust中不会产生垂悬引用。
这段代码修改如下,即可编译通过:
1{
2 let x = 5; // ----------+-- 'b
3 // |
4 let r = &x; // --+-- 'a |
5 // | |
6 println!("r: {}", r); // | |
7 // --+ |
8} // ----------+
修改后的代码r
的生命周期为'a
小于其引用的值x
的生命周期'b
,可以编译通过。
以上这两个代码例子中,引用的生命周期是隐式可推断的,Rust的编译器通过分析生命周期来保证引用总是有效的。
1.2 无法推断的情况 #
接下来,来看稍微复杂一些的情况,在这种情况下引用的生命周期是无法隐式推断出的。例如:
1fn longest(x: &str, y: &str) -> &str { // missing lifetime specifier
2 if x.len() > y.len() {
3 x // explicit lifetime required in the type of `x`
4 } else {
5 y // explicit lifetime required in the type of `y`
6
7 }
8}
longest
的功能是返回两个字符串中较长的字符串,它的参数x
和y
传的是引用(字符串slice),不会获取所有权。
在编译这个函数的时候并不清楚将来怎么调用这个函数,传的是什么参数,这个函数出现了多个引用参数x
和y
,它们的生命周期可能不一样,不能确定实际执行的是if
还是else
,函数返回的生命周期也不好确定。
即编译器无法隐式推断出这些引用的生命周期。从这段代码longest
函数的编译错误(missing lifetime specifier
)可以看出,在这种情况下,需要使用的生命周期注解来显示标注引用的生命周期。
先来看一下生命周期注解的语法: 生命周期参数名称必须以撇号'
开头,其名称通常全是小写,'a
是大多数人默认使用的名称。生命周期参数注解位于引用的&
之后,并有一个空格来将引用类型与生命周期注解分隔开。
1&i32 // 引用
2&'a i32 // 带有显式生命周期的引用
3&'a mut i32 // 带有显式生命周期的可变引用
加上生命周期注解之后,并不会改变引用的生命周期长短。生命周期注解描述了多个引用生命周期相互的关系。例如函数的参数和返回值加上生命周期注解后,描述的是参数与参数之间、参数与返回值之间的关系,不会改变原有生命周期。
下面将longest
函数的参数和返回值使用生命周期注解标记如下:
1fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
2 if x.len() > y.len() {
3 x
4 } else {
5 y
6 }
7}
标记的结果表达了函数签名中的参数、返回值三个引用的必须有相同的生命周期'a
。
因为实际调用longest函数时,传入参数的生命周期长短可能不一样,所以得出的隐含的条件是longest函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。
这样在编译时编译器就可以以此为条件进行检查。不满足这个条件的函数调用将会被编译器中的借用检查器拒绝。
下面在三个不同的main函数中调用longest
函数,分析一下编译器如何根据生命周期参数进行检查的。
例1:
1fn main() {
2 let string1 = String::from("abcd");
3 let string2 = String::from("xyz");
4 let result = longest(string1.as_str(), string2.as_str());
5 println!("The longest string is {}", result);
6}
上面的代码在调用longest
函数时,string1
, string2
的引用被传递给longest
,根据借用规则引用不能超过值的生命周期,所以这里'a
表示的具体的生命周期不能超过string1
和string2
中较小的那个生命周期。
返回的result
生命周期是'a
,即result
的生命周期要小于string1
和string2
中较小的那个生命周期。实际上result的生命周期 < string2的生命周期 < string1的生命周期
,满足条件,所以这段代码能编译通过。
例2:
1fn main() {
2 let string1 = String::from("long string is long");
3
4 {
5 let string2 = String::from("xyz");
6 let result = longest(string1.as_str(), string2.as_str());
7 println!("The longest string is {}", result);
8 }
9}
根据在例1中的分析,返回的result
生命周期是'a
,即result
的生命周期要小于string1
和string2
中较小的那个生命周期。
实际上result的生命周期 < string2的生命周期 < string1的生命周期
,满足条件,所以这段代码能编译通过。
例3:
1fn main() {
2 let string1 = String::from("long string is long");
3 let result;
4 {
5 let string2 = String::from("xyz");
6 result = longest(string1.as_str(), string2.as_str()); // `string2` does not live long enough
7
8 }
9 println!("The longest string is {}", result);
10}
根据在例1中的分析,返回的result
生命周期是'a
,即result
的生命周期要小于string1
和string2
中较小的那个生命周期。
实际上: result的生命周期 > string2的生命周期
, string2的生命周期 < string1的生命周期
,不满足条件,所以这段代码无法编译通过。
2.静态生命周期 #
前面通过几个例子深入理解了引用的生命周期不能超过其值的生命周期。在Rust中如果一个值是在某个作用域上定义的,它可能会被分配到栈上,也可能会被分配到堆上:
- 栈上的值的生命周期和栈帧的生命周期一致,这同其他编程语言一样。
- 堆上分配的值的生命周期在所有权模型下与其在栈上的所有者变量的生命周期一致,这同其他编程语言不同。在其他编程语言中,堆内存的生命周期是不明确的,有的需要开发人员手工维护,有的需要语言的运行时进行额外的检查(GC)。(注意:Rust中还提供了另外一种机制,使Rust代码可以创建不受栈内存生命周期控制的堆内存,这样在编译时就可以绕过所有权检查。为什么Rust会提供这样一种机制,使用场景是什么,我们将在后边学习。)
除了上面这两种动态生命周期外,在Rust中还有一种特殊的生命周期。
在Rust中如果一个值的生命周期能够存活于整个程序进程的生命周期,就将其称之为静态生命周期。静态生命周期用'static
表示。当值具有静态生命周期时,其引用也具有静态生命周期,可以使用'static
进行标注。
例如,在Rust中所有的字符串字面值具有静态生命周期:
1let s = "hello";
2let s: &'static str = "hello";
Rust中的全局变量(静态变量)、常量(Const)、字符串字面量具有静态生命周期,贯穿整个程序进程的生命周期。
例4:
1fn main() {
2 let string1 = String::from("long string is long");
3 let result;
4 {
5 let string2 = "xyz";
6 result = longest(string1.as_str(), string2);
7
8 }
9 println!("The longest string is {}", result);
10}
11
12
13fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
14 if x.len() > y.len() {
15 x
16 } else {
17 y
18 }
19}
上面例4的代码是将前面例3中的string2
修改成字符串字面量,此时result的生命周期 < string1的生命周期 < string2的生命周期(静态生命周期)
,所以例4可以编译通过。
3.省略生命周期标注 #
Rust将Rust程序员最常用的生命周期注解编写模式整合到Rust的编译器中,这样在这些模式下,编译器中的借用检查器就可以自动推断出生命周期,而不再强制我们显式的添加生命周期注解。 被整合进Rust编译器的引用分析模式被称为生命周期省略规则。生命周期省略规则并不是让程序员遵守的规则,而是规定了一些特殊的场景,编译器会自动考虑这些场景,无需我们显式提供生命周期注解。 但如果Rust编译器在使用这些规则的前提下,仍然无法推断引用生命周期,还是会编译出错,此时还是需要我们显式标注生命周期。
函数或方法的参数的生命周期被称为输入生命周期(input lifetimes),而返回值的生命周期被称为输出生命周期(output lifetimes)。编译器采用三条规则来判断引用何时不需要明确的生命周期注解:
- 规则1: 每一个是引用的参数都有它自己的生命周期参数。意思就是有一个引用参数的函数有一个生命周期参数:
fn foo<'a>(x: &'a i32)
,有两个引用参数的函数有两个不同的生命周期参数,fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
,依此类推。 - 规则2: 如果只有一个输入生命周期参数,那么它被赋予所有输出生命周期参数。例如:
fn foo<'a>(x: &'a i32) -> &'a i32
. - 规则3: 如果方法有多个输入生命周期参数并且其中一个参数是
&self
或&mut self
,self
的生命周期会赋予所有输出生命周期。
第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。这些规则适用于fn
定义,以及impl
块。
学习了生命周期省略规则,我们再来看前面的longest
函数,如果不显式添加生命周期注解的话,会被编译器根据生命周期省略规则中的规则1自动添加生命周期注解,如下:
1fn longest(x: & str, y: & str) -> & str {
2 if x.len() > y.len() {
3 x
4 } else {
5 y
6 }
7}
8
9↓
10↓ 编译器根据规则1自动添加生命周期注解
11↓
12
13fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'? str {
14 if x.len() > y.len() {
15 x
16 } else {
17 y
18 }
19}
注意根据规则1
,自动给参数x, y添加了生命周期注解,但因为longest
函数的签名不满足规则2
和规则3
,所以无法自动为返回值添加输出生命周期注解。对于返回值如何标注编译器是无能为力的,只能由我们显式标注。
对于下面的代码,函数签名同时满足规则1
和规则2
,所以即使省略生命周期注解,也是可以编译通过的:
1fn first_word(s: &str) -> &str {
2 let bytes = s.as_bytes();
3 for (i, &item) in bytes.iter().enumerate() {
4 if item == b' ' {
5 return &s[0..i];
6 }
7 }
8 &s[..]
9}
10
11↓
12↓ 编译器根据规则1和规则2自动添加生命周期注解
13↓
14
15fn first_word<'a>(s: &'a str) -> &'a str {
16 let bytes = s.as_bytes();
17 for (i, &item) in bytes.iter().enumerate() {
18 if item == b' ' {
19 return &s[0..i];
20 }
21 }
22 &s[..]
23}