0. Intro
Rust引用是有生命周期的,即引用有一个有效作用域:
- 一般而言,引用的生命周期是隐式推导的
- 少部分需要使用泛型生命周期参数来注明
1. Revision: 悬垂引用
Rust有借用检查器,用于检查引用的在其所在的作用域中都是有效的。例如下面这个例子,就会报悬垂引用的错误:
1
2
3
4
5
6
7
8
9
{
let r;
{
let x = 5; // x owns 5
r = &x; // r ref/borrows x
}
// 5 that x owns is freed, the r is dangling
println!("r: {}", r);
}
但是,有时候,Rust没有获得足够的信息来检查生命周期时,会要求我们显式标注生命周期。下面的例子也会报错(编译器不知道返回的引用是来自x
还是y
):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fn longest(x: &str, y: &str) -> &str {
if x.len() < y.len() {
y
} else {
x
}
}
/*
error[E0106]: missing lifetime specifier
--> src/main.rs:1:33
|
1 | fn longest(x: &str, y: &str) -> &str {
| ^ expected lifetime parameter
|
= help: this function's return type contains a borrowed value, but the
signature does not say whether it is borrowed from `x` or `y`
*/
为了解决上面的问题,Rust引入泛型生命周期参数,定义引用间的关系,以让借用检查器可以进行分析。
2. 泛型生命周期注解
泛型生命周期注解的作用:
- 描述多个引用间生命周期的关系
- 不影响生命周期
这类注解的语法比较特别,其参数必须以'
开头,例如'a
,'b
等。
通常一般都会写'a
,如下所示:
1
2
3
&T // 引用
&'a T // 显式生命周期标记的引用
&‘a mut T // 显式生命周期标记的可变引用
不过,由于这类注解用于描述多个引用的生命周期的关系,所以作用于单个引用是没用的,应该作用于多个引用间。
假如ref1
和ref2
都被标记了'a
,那么这2个引用的生命周期会被编译器视作一样。
2.1. 函数的生命周期注解
在1中的longest
函数改成下面的,给参数和返回值标记'a
生命周期,即可通过编译:
1
2
3
4
5
6
7
fn longest(x: &’a str, y: &'a str) -> &'a str {
if x.len() < y.len() {
y
} else {
x
}
}
在该函数中,编译器认为:相同标记的参数和返回值引用,它们拥有相同生命周期,且取它们重叠的/最短的。
所以下面2个例子,一个能通过编译,一个不能通过,可看代码注释的解释:
-
通过编译的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
fn main() { // main let string1 = String::from("long string is long"); {// inner-scope let string2 = String::from("xyz"); // string1.as_str()引用生命周期在main // string2.as_str()引用生命周期在inner-scope // 函数中它们和返回值被标记生命周期'a // 所以,编译器认为它们以及返回值result的生命周期在inner-scope let result = longest(string1.as_str(), string2.as_str()); // 所以这里是result是有效的,编译通过 println!("The longest string is {}", result); } }
-
不能通过编译的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
fn main() { // main let string1 = String::from("long string is long"); let result; { // inner-scope let string2 = String::from("xyz"); // string1.as_str()引用生命周期在main // string2.as_str()引用生命周期在inner-scope // 函数中它们和返回值被标记生命周期'a // 所以,编译器认为它们以及返回值result的生命周期在inner-scope result = longest(string1.as_str(), string2.as_str()); } // 但这里result在main中,脱离了inner-scope,引用无效/悬垂,编译失败 println!("The longest string is {}", result); }
1 2 3 4 5 6
fn longest<'a>(x: &str, y: &str) -> &'a str { let result = String::from("really long string"); // 这里返回引用的生命周期最多只能在longest内(取最小),即'a为longest // 那么调用该函数后,返回值将成为悬垂引用,编译失败 result.as_str() }
2.2. 方法的生命周期注解
方法的生命周期注解需要在impl
开头注明出来,如下所示:
1
2
3
impl<'a> T { // &self and &mut self are marked with 'a
// method implementations ...
}
有了上面impl
的'a
注解,方法中的&self
以及&mut self
参数都会有'a
的注解;而且,若返回值是引用,默认也会有一样的生命周期注解。
方法中其它的参数和返回值请参考2.1.,规则一样适用。
2.3. 结构的生命周期注解
若结构体的字段是引用,那么这个引用就需要生命周期注解(否则编译器就无法保证结构存活时引用的的有效性)。
结构体中的生命周期注解是为了:保证引用在结构体存活时有效。
例如下面的例子,引用part
在Example
生命周期内有效:
1
2
3
struct Example<'a> {
part: &'a str // 编译器保证part在Example生命周期内有效
}
2.4. 静态生命周期
可使用‘static
标注一个引用是静态的,其生命周期能够存活于整个程序运行期间。
例如,所有字符串字面值都有静态生命周期,即:
1
let s: &'static str = "str";
3. 生命周期省略规则
对一些特殊场景,编译器会允许我们省略生命周期注解。它满足一定的规则。
不过在这之前先明确2个定义:
- 输入生命周期:参数的生命周期
- 输出生命周期:返回值的生命周期
编译器有3条生命周期省略规则,其应用于fn
和impl
块:
-
每个是引用的参数都有自己的生命周期注解,若不指定,它们的生命周期注解互不相同,即:
1 2 3 4 5
/* fn func(a: &T) <==> fn func<'a>(a: &'a T) fn func(a: &U, b: &V) <==> fn func<'a, 'b>(a: &'a T, b: &'b T) 以此类推 */
-
若输入的引用参数只有1个,且只有1个生命周期注解,则返回引用的生命周期注解和参数的一样,即:
1 2 3 4
/* fn func(a: &T) -> &U <==> fn func(a: &'a T) -> &U // Rule 1 <==> fn func(a: &'a T) ->&'a U // Rule 2 */
-
对于方法,若参数有很多生命周期注解,但由于有
&self/&mut self
参数的缘故,返回引用的生命周期注解和self
一样,即:1 2 3 4 5 6 7 8 9 10 11 12 13 14
/* impl<'a> T { fn func(&self, arg: &U) -> &V { // ... } } 等价于: impl<'a> T { fn func(&'a self, arg: &'b U) -> &'a V { // Rule 1 and Rule 3 // self生命周期注解为'a,它和返回引用的生命周期注解一样 // ... } } */