1.11 生命周期(进阶) Pt.1:回顾、借用检查器、泛型生命周期
这篇文章在已有基础上对生命周期这一概念进行补充。
1.11.1. 回顾
在初级教程中我们提到过:Rust里每个引用都有生命周期,它就是引用保持合法的作用域(scope),大多数时候都是隐式并且由编译器推断出来的。
对某个变量取得引用时生命周期开始,当变量移动或离开作用域时生命周期结束。也就是对于某个引用来说,它必须保持合法的一个代码区域的名称。
生命周期通常与作用域重合,但也不一定。
1.11.2. 借用检查器(Borrow Checker)
每当具有某个生命周期'a的引用被使用,借用检查器都会检查'a是否还存活。具体方法是:
- 追踪路径到
'a开始(获得引用)的地方 - 从这开始,检查沿着路径是否存在冲突
- 保证引用指向一个可安全访问的值
这个例子使用了 rand crate。在Cargo.toml中添加如下依赖:
[dependencies]
rand = "0.8"
看个例子:
use rand::random;
fn main() {
let mut x = Box::new(42);
let r = &x;
if random::<f32>() > 0.5 {
*x = 84;
} else {
println!("{}", r);
}
}
-
x是Box<i32>类型 -
把
r声明为x的引用,从这一行(第5行)开始引用的生命周期就开始了 -
第7行对
x进行了解引用修改值的操作,需要一个指向x的可变引用。借用检查器此时就会去出一个指向x的可变引用,并检查它的使用是否存在冲突,这个代码例中没有冲突,所以代码是合法的 -
有人可能会问了:第7行在
r的作用域里,*x需要指向x的可变引用,那在同一个作用域里既有不可变引用r和可变引用*x违反了借用规则不应该报错吗? 事实上Rust很聪明,知道代码如果走了if分支就不能走else分支了,r在if分支下根本就没有使用过,所以在if分支下使用*x这个可变引用是没有问题的。换句话说,r的生命周期并没有延伸到if分支里。这就是生命周期通常不与作用域完全重合的例子
再看一个例子:
fn main() {
let mut x = Box::new(42);
let mut z = &x;
for i in 0..100 {
println!("{}", z);
x = Box::new(i);
z = &x;
}
println!("{}", z);
}
x是Box<i32>类型z是x的引用,从这行(第4行)生命周期就开始了- 第6行,循环中打印了
z,使用到了z这个引用自然就会被借用检查器检查。这里没有什么问题,所以借用检查器不会报错 - 第7行,
x被重新赋值 - 第8行,
z被重新赋值,Rust会把新赋的这个引用视作是另一个引用,所以相当于第8行是新的生命周期,而原本的生命周期到7行就结束了 - 之后的每次循环都是
z = &x;这一行会开启一个新的生命周期。借用检查器因此不会报错
借用检查器的特性
借用检查器是保守的:如果不确定某个借用是否合法,借用检查器就会拒绝该借用。
借用检查器有时候会需要帮助来理解借用为什么是合法的,这就是Unsafe Rust存在的部分原因。
1.11.3. 泛型生命周期
有时候我们需要在自己的类型里储存引用。我们就需要给这些引用标注生命周期,以便借用检查器检查合法性。例如在类型方法中返回引用,且存活时间比self长。
Rust允许你基于一个或多个生命周期将类型的定义泛型化。
两点提醒
-
如果类型实现了
Droptrait,那么丢弃类型时,就被记作是使用了类型所泛型的生命周期或类型。如果类型没有实现Droptrait,那么类型丢弃时不会当作使用了生命周期,可以忽略类型内的引用。 举个例子:某个类型实例要被丢弃了,在丢弃之前,借用检查器会查看是否仍然合法地去使用你类型的泛型生命周期,因为你在drop函数中的代码可能会用到这些引用 -
类型可泛型多个生命周期,但通常没必要让类型签名变得更复杂。只有类型包含多个引用时,你才应该使用多个生命周期参数,并且返回的引用只应绑定到其中一个引用的生命周期。
看个例子:
#![allow(unused)]
fn main() {
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
}
'a表示有a这么一个生命周期,x、y以及返回类型都是这个生命周期a,这个时候就表示x、y和返回类型的生命周期是一样的。