这可能是继所有权之后又一个Rust难点内容
1 生命周期的作用
首先了解一下什么是生命周期。生命周期是用作引用的概念,主要作用是保证引用在其作用域内指向的数据是有效的,即避免悬垂引用(dangling reference)。
fn main() {
{
let r;
{
let x = 5;
r = &x; // 报错,因为x离开作用域后r变成一个悬垂引用
}
println!("r: {}", r);
}
}
在之前的学习中了解过引用,Rust中的引用有点类似C/C++的指针,但是笔者认为Rust中的引用还有其他的特性。为了保证所有的引用都是有效的(在作用域内数据都是安全的),Rust中所有的引用都有自己的生命周期,只不过在大多数情况下编译器可以自动推断引用的生命周期,不需要程序员手动标注;如果编译器不能推断引用的生命周期,那么就需要程序员标注出生命周期来帮助编译器编译程序,如果编译器无法获取足够的生命周期信息,编译器就会直接报错。
Rust如何确定借用不合法呢,答案是使用了借用检查器。它的运行规则是,被引用者的生命周期不能小于引用者的生命周期。例如在上面的例子中,x的生命周期小于了r的生命周期,所以编译器就直接报错了。
2 函数或者方法中的生命周期
函数或者方法中的参数或者返回值为引用时,就需要考虑生命周期的问题了。因为函数定义时,我们并不知道函数外部的数据什么时候会被销毁(Drop),这样就有可能使得函数的返回值(类型为引用)成为一个悬垂引用。例如下面的例子:
fn main() {
let string1 = String::from("hello");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
生命周期参数也是泛型,称为泛型生命周期参数,标准方法如下:
&i32
:i32的引用&'a i32
:标注生命周期的i32引用&'a mut i32
:标注生命周期的可变的i32引用
单个生命周期的标注没有意义,生命周期的标注并不会影响具体的生命周期的长度,它只是标注了参数和返回值之间生命周期的连接关系。例如在上面的例子中,仅仅说明了参数的生命周期和返回值的生命周期是相同的,这就保证了只要实参是有效的,返回值就必须有效,这样编译器就可以在main函数中检查所有数据是否一直有效。
对于上面例子中的函数longest
的两个参数都标记为'a
,它实际上的作用是表示返回值的生命周期和两个实参变量中较短的保持一致,因为生命周期短的数据有效时长的一定有效。如果在这个规则下返回的引用有可能失效,那么编译器就会直接报错。
指定生命周期参数的方式取决于函数的具体功能。从函数返回引用时,返回类型的生命周期一定要和其中一个参数(实参)的生命周期相关联,如果返回的引用没有指向任何参数,那么这个引用指向的值只能由函数内部创建,当程序走出函数时这个值就会失效,那么返回的引用就将称为悬垂引用,Rust当然不会允许这种情况的方式。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
let string = String::from("hello");
string.as_str() // 报错
}
3 结构体和方法中的生命周期
3.1 结构体字段(成员)的生命周期
如果结构体的成员是一个引用,那么就需要标注这个引用的生命周期。
struct ImportantExpect<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me ishmael. Some years ago...");
let first_sentence = novel.split('.')
.next()
.expect("Could not find a '.'"); // 返回一个字符串切片
let i = ImportantExpect {
part: first_sentence,
};
}
在结构体中,引用的生命周期的意思是成员的存活时间必须不短于结构体实例的时间。总不能结构体实例还存在时,结构体成员的内存就被释放吧?
3.2 方法的生命周期
由于方法的函数参数往往包含&self
或者&mut self
,应用下面的说明的生命周期省略规则的第三条,我们往往不需要标注方法的生命周期参数。但是为含有生命周期参数的结构体实现方法时,需要注意语法:impl<'a>
后面的<'a>
不能省略;结构体名后面的<'a>
也不能省略,因为它是结构体名字的一部分。
impl<'a> ImportantExpect<'a> {
fn level(&self) -> i32 {
3
}
}
4 生命周期省略标注的规则
在Rust早期版本,函数返回引用时必须标注生命周期,后来在Rust引用分析中发现很多生命周期是可以由编译器确定的,所以引入了引用分析中所编入的模式,称为生命周期省略规则。但是这个省略规则并不会提供完整的推断,如果在适用这些规则后,引用的生命周期仍然模糊不清,那么编译器就会直接抛出错误。
- (适用于输入生命周期)每个引用类型的参数都有自己的生命周期参数;
- (适用于输出生命周期)如果只有1个输入生命周期参数,那么该生命周期就会被赋给所有的输出生命周期参数;
- (适用于输出生命周期)如果有多个输入生命周期参数,但是其中一个是
&self
或者&mut self
,那么self
的生命周期参数就会被赋值给所有的输出生命周期参数。
在上面的规则中,如果生命周期出现在函数/方法的参数中,那么这个生命周期就称为输入生命周期;如果这个生命周期出现在函数/方法的返回值中,那么这个生命周期就称为输出生命周期。
此外,有一个特殊的生命周期'static
,它表示整个程序的运行时间,字符串子面值的生命周期就是'static
。有时候,在编译报错提示信息时会提示我们使用'static
来修复程序中的一些错误,但是无脑的使用静态生命周期是不合理的。