生命周期
生命周期
什么是一个变量的“生命周期”
变量的“生命”指的是变量的有效范围,也就是从变量创建开始,到变量离开作用域结束。一个值(或变量)“活着”,就是它还在作用域内,尚未被释放(还没被 drop)。
而当变量离开作用域或被提前释放,我们就说它“死了”
let r; // r 在外层作用域声明
{
let x = 5;
r = &x; // ❌ 在这个括号结束的时候x被释放结束了他的生命,但是r还在引用x
}
println!("{}", r); // r 指向一个“死了”的 xx这个变量只在花括号中有效,出了花括号就被释放掉了,生命就结束了,r"活着"的时间明显大于x,所以在r借用x后可能会出现在使用r时x已经“死了”的情况,这就是悬垂引用。这里生命开始到生命结束的时间就是变量的生命周期。
生命周期要解决的问题
从什么我们可以发现,在一个变量借用另一个变量时,借用方的生命周期不能大于被借用方的生命周期,否则可能会出现悬垂引用。
那么在下面这种情况中我们如何划定生命周期?
// 假设a的生命周期大于b的生命周期,那么返回值c的生命周期应该是和a一样大还是和b一样大呢?
fn a_or_b(a: &i32, b: &i32) -> &i32 {
let c: &i32;
if *a > *b { c = a }
else { c = b }
c
}// 我们先假设c的生命周期应该和a一样大,且a的生命周期大于b看会发生什么
let a = /*i32*/;
let c: &i32;
{
let b = /*i32*/;
c = a_or_b(a, b);
}
println!("{}", c);
// 场景一:如果a的值大于b时,c借用a,这个时候c的生命周期和a一样大,看上去好像没什么问题
// 场景二:如果a的值等于b时,c借用b,可是b的生命周期在括号结束的时候就已经结束了
// 我们发现引用者c的生命周期大于可能的被引用者b, 即a_or_b这个方法返回的值可能会出现垂直引用当我们在借用一个值时,我们要知道这个值在什么时候会被销毁,而且我们应该避免在这个值的生命周期外继续借用这个值,但是我们在现实生产中很多时候没办法直接判断出每一个值的生命周期,特别是我们在设计一些接口和结构的时候,我们没办法知道参数的生命周期,因为我们不知道我们的接口和结构会以一种什么方式被调用。
这里我们不妨先将函数或者方法中的生命周期进行划分,外部的生命周期主要是参数
生命周期标注
为了确保引用永远都不超过其引用值的生命周期,Rust 要求我们在声明一个引用类型时对其进行生命周期标注,来告诉编译器其生命周期
// 这里标注了一个生命周期'a, 告诉编译器a、b和c的生命周期至少要大于等于'a的生命周期
// 在使用这个方法时,如果参数的生命周期小于返回值的生命周期,编译器就会把这个参数标记出来告诉开发者并报错
fn a_or_b<'a>(a: &'a i32, b: &'a i32) -> &'a i32 {
if *a > *b { a }
else { b }
}比如下面这种使用方法在编译时编译器就会报错
let a = /*i32*/;
let c: &'a i32;
{
let b = /*i32*/;
c = a_or_b(&a, &b); // 这里b的生命周期明显小于c的生命周期,在后续就可能会导致悬空指针,我们在编译时就应避免这种情况
// 正确的做法是把c的声明拿到括号内或者把b拿出去,这样无论a或b的值谁更大,对c来说都他们的声明周期都大于等于c
}
println!("{}", c);三条消除规则
我们如果为每一个作用域的引用类型都标注生命周期,对与开发来说也过于繁琐了,所以Rust为我们提供了三条消除规则,来自动标注生命周期。
1.每一个引用参数都会获得独自的生命周期
例如一个引用参数的函数就有一个生命周期标注:
// 两者等同,这里编译器帮我们隐式的标注了生命周期
fn foo(x: &i32)
fn foo<'a>(x: &'a i32)两个引用参数的有两个生命周期标注,依此类推。
// 两者等同
fn foo(x: &i32, y: &i32)
fn foo<'a, 'b>(x: &'a i32, y: &'b i32)2.若只有一个输入生命周期(参数中只有一个引用类型),那么该生命周期会被赋给所有的输出生命周期
例如:
// x 参数的生命周期会被自动赋给返回值`&i32`,因此两者等同
fn foo(x: &i32) -> &i32
fn foo<'a>(x: &'a i32) -> &'a i323.若存在多个输入生命周期,且其中一个是&self或&mut self,则&self的生命周期被赋给所有的输出生命周期
// 两者等同
fn bar(&self, lf: &i32) -> &i32
fn bar(&'a self, lf: &'b i32) -> &'a i32拥有&self形式的参数,说明该函数是一个方法,该规则让方法的使用便利度大幅提升。
编译器使用三条消除规则来确定哪些场景不需要显式地去标注生命周期,这极大的方便了我们的开发工作。其中第一条规则应用在输入生命周期上,第二、三条应用在输出生命周期上。若编译器发现三条规则都不适用时,就会报错,提示你需要手动标注生命周期。
但是编译器的消除规则不是万能的,若编译器不能确定某件事是正确时,会直接判为不正确,那么你还是需要手动标注生命周期