喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
这篇文章在Rust初级教程的基础上对生命周期这一概念进行了补充,建议先看【Rust自学】专栏的第10章的文章。
1.11.1. 回顾
在初级教程中我们提到过:Rust里每个引用都有生命周期,它就是引用保持合法的作用域(scope),大多数时候都是隐式并且由编译器推断出来的。
对某个变量取得引用时生命周期开始,当变量移动或离开作用域时生命周期结束。也就是对于某个引用来说,它必须保持合法的一个代码区域的名称。
生命周期通常与作用域重合,但也不一定。
1.11.2. 借用检查器(Borrow Checker)
每当具有某个生命周期'a
的引用被使用,借用检查器都会检查'a
是否还存活。具体方法是:
- 追踪路径到
'a
开始(获得引用)的地方 - 从这开始,检查沿着路径是否存在冲突
- 保证引用指向一个可安全访问的值
看个例子:
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允许你基于一个或多个生命周期将类型的定义泛型化。
两点提醒
-
如果类型实现了
Drop
trait,那么丢弃类型时,就被记作是使用了类型所泛型的生命周期或类型。如果类型没有实现Drop
trait,那么类型丢弃时不会当作使用了生命周期,可以忽略类型内的引用。
举个例子:某个类型实例要被丢弃了,在丢弃之前,借用检查器会查看是否仍然合法地去使用你类型的泛型生命周期,因为你在drop
函数中的代码可能会用到这些引用 -
类型可泛型多个生命周期,但通常没必要让类型签名变得更复杂。只有类型包含多个引用时,你才应该使用多个生命周期参数,并且返回的引用只应绑定到其中一个引用的生命周期。
看个例子:
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
和返回类型的生命周期是一样的。