生命周期是 rust 语言的一大特点,同时也是非常难的一部分,初学时比较难以理解
那么 rust 为什么需要它呢?
我们需要从指针
开始说起
传统的指针
c、c++ 语言中,可以定义指针,例如下面的 c 代码
int* getDanglingPointer() {
int localVar = 42; // 局部变量在栈上分配
return &localVar; // 返回局部变量的地址 - 危险!
}
上面代码的问题显而易见,指针(引用)被返回,但是指针指向的变量是函数内部的局部变量,函数调用结束之后已经被释放了,该变量这块内存已经不知道是被那个程序使用了,此时再使用指针,取到的值就不再是预期中的值了,轻则 bug,重则直接影响到整个程序,导致崩溃,非常危险。
当然绝大多数情况下不会有人写出上面的代码,它只是为了说明,在语言层面 c、c++ 不会对这种代码做处理,因为语法是正确的,只有在真正运行时才会发生错误。
那避免空指针问题就只能取决于开发者的水平了,可是复杂项目中可能会发生空指针的情况会比较隐蔽,即便有代码审查,也会存在空指针的风险。
而 rust 使用借用检查器配合生命周期解决了这个问题
什么是生命周期
生命周期 = 引用的有效范围
存在引用就必然涉及引用的有效范围,因为任何一个引用的有效范围都不能超过其引用的值
rust 中生命周期解决的一个核心问题就是空指针,它要保证所有引用是合法的,不会存在空指针的情况。
上面的 c 代码在 rust 中的写法无法通过编译,编译器会直接提示错误
道理很简单:引用的有效范围超过了其引用的值
fn get_dangling_pointer() -> &i32 {
let local_var = 1;
return &local_var;
}
红色波浪线就是编译器对于开发者的警示:rust 编译器很智能,发现这是一个必然会发生空指针的函数,所以会提示错误及修改意见
生命周期参数
编译器也并非完全智能,以下代码编译器就不能直接判断是否会存在问题
fn longer(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
上述函数传入两个字符串切片(实际也是引用),返回长度较长的一个
此时问题来了:返回的是一个引用,来自于 x 或者 y,不确定是哪一个。甚至函数调用之后 x、y 是否还有效都无从知晓,编译器无法确定返回的引用和输出之间的生命周期关系
,可能会存在悬垂引用
我们现在可以先单独来分析函数:
假设参数 x 的作用域为 a,参数 y 的作用域为 b
由于返回值是 x 或者 y,那么返回值的作用域要小于等于
x y 中较小的那个作用域
满足上面的条件才能保证函数返回的引用指向是有效的
上面的函数在编译时 rust 就会提示,缺少生命周期标识
此种情况需要为引用类型添加生命周期约束,如下
fn longer<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
要注意的是,其中生命周期参数 'a
并不直接表示 x 和 y 的生命周期就是 'a
'a
表示一种约束:x 和 y 的真实生命周期必须大于等于'a
生命周期
同时返回值的生命周期必须小于等于 'a
的确有一点绕,还有一种写法较为复杂,但是理解上其实更为直观,如下
// 'x: 'y 的写法表示 生命周期x >= 生命周期y
fn longer_full<'a: 'c, 'b: 'c, 'c>(x: &'a str, y: &'b str) -> &'c str {
if x.len() > y.len() {
x
} else {
y
}
}
以上代码实际上对三个引用都约束了生命周期(也就是作用域)x 和 y 以及返回的引用都有自己的生命周期约束,同时两个输入的生命周期都要 大于等于 最终的输出生命周期,我们并没有必要这么写,只是便于理解
生命周期消除规则
根据前面的代码示例和逻辑我们现在可以知道:对于函数,只有同时存在输入引用和输出引用时才需要标注生命周期
但有的时候即便同时存在输入引用和输出引用也无需手动标注生命周期,例如
fn an_example(s: &str) -> &str {
// ...
}
上面的函数虽然同时存在输入和输出引用,但是我们甚至不需要关注具体内容就知道无需标注生命周期,原因在于:函数返回了一个引用,那么这个引用只可能来自两个地方
- 引用了函数内部的数据
- 引用来自于函数参数
对于第一种情况,必定会返回悬垂指针,编译器直接会提示错误
现在只剩下了第二种:那么就意味着输出的引用与输入引用强相关
此时我们就不需要再标注生命周期了,编译器会自动推断,这就是生命周期消除
关于消除规则,有如下几条
- 每一个引用参数都会获得独自的生命周期
- 若只有一个输入生命周期,那么该生命周期会被赋予给所有的输出生命周期
- 若存在多个输入生命周期,且其中一个是 &self 或 &mut self,则 &self 的生命周期被赋给所有的输出生命周期
以上都是编译器给予的便利,不需要在这些情况下标注生命周期,除此之外,就需要我们手动标注了
代码示例1
#[derive(Debug)]
struct Foo;
impl Foo {
fn mutate_and_share(&mut self) -> &Self {
&*self
}
fn share(&self) {}
}
fn main() {
let mut foo = Foo;
let loan = foo.mutate_and_share();
foo.share();
println!("{:?}", loan);
}
上面代码编译时会提示
cannot borrow `foo` as immutable because it is also borrowed as mutable
同时获取了 foo 的可变借用和不可变借用
原因如图
方法 mutate_and_share 满足生命周期消除规则,输入的生命周期被赋予给输出生命周期,也就是当 loan
有效时,对于 foo 的可变引用也同样有效,但是可变引用有效期间又同时尝试获取不可变引用,借用检查器就会告知编译错误
把最后一行注释掉,就可以解决此冲突
#[derive(Debug)]
struct Foo;
impl Foo {
fn mutate_and_share(&mut self) -> &Self {
&*self
}
fn share(&self) {}
}
fn main() {
let mut foo = Foo;
let loan = foo.mutate_and_share();
// loan 生命周期到此结束,同时可变引用也结束,此时再获取不可变引用,将不会发生冲突
foo.share();
// println!("{:?}", loan);
}
代码示例2
#![allow(unused)]
fn main() {
use std::collections::HashMap;
use std::hash::Hash;
fn get_default<'m, K, V>(map: &'m mut HashMap<K, V>, key: K) -> &'m mut V
where
K: Clone + Eq + Hash,
V: Default,
{
match map.get_mut(&key) {
Some(value) => value,
None => {
map.insert(key.clone(), V::default());
map.get_mut(&key).unwrap()
}
}
}
}
以上代码同样会提示错误
cannot borrow `*map` as mutable more than once at a time
不能同一时间有多个可变借用
主要原因在 match 表达式中,分析如下
这段代码出错的原因来自于生命周期消除规则和 match 表达式规则的结合
当一个引用作为 match 表达式的 “判断条件” 时:
- 如果这个引用不需要在 match 分支中继续使用或返回,那么它的生命周期就仅限于用于模式匹配的那一刻
- 如果这个引用需要在某个分支中返回或继续使用,那么它的生命周期就会延续到整个 match 表达式结束
修改后可通过编译的代码
#![allow(unused)]
fn main() {
use std::collections::HashMap;
use std::hash::Hash;
fn get_default<'m, K, V>(map: &'m mut HashMap<K, V>, key: K) -> &'m mut V
where
K: Clone + Eq + Hash,
V: Default,
{
// 获取的不可变引用
match map.get(&key) {
Some(value) => {
// value 在后续并未使用,所以不可变引用的生命周期已经结束
map.get_mut(&key).unwrap()
},
None => {
map.insert(key.clone(), V::default());
map.get_mut(&key).unwrap()
}
}
}
}
综合示例
一个综合的生命周期错误示例
struct Interface<'a> {
manager: &'a mut Manager<'a>
}
impl<'a> Interface<'a> {
pub fn noop(self) {
println!("interface consumed");
}
}
struct Manager<'a> {
text: &'a str
}
struct List<'a> {
manager: Manager<'a>,
}
impl<'a> List<'a> {
// 此处生命周期参数 'a 是编译错误的核心原因
pub fn get_interface(&'a mut self) -> Interface {
Interface {
manager: &mut self.manager
}
}
}
fn main() {
let mut list = List {
manager: Manager {
text: "hello"
}
};
list.get_interface().noop();
println!("Interface should be dropped here and the borrow released");
// 下面的调用会失败,编译器提示同时有不可变/可变借用
use_list(&list);
}
fn use_list(list: &List) {
println!("{}", list.manager.text);
}
先分析代码,这是 Manager
定义
struct Manager<'a> {
text: &'a str
}
此处可以分析出
Manager
的生命周期参数'a
实际上就是text
的生命周期
同时表明:整个结构体的生命周期不能大于这个字符串切片的生命周期
结构体 Interface
struct Interface<'a> {
manager: &'a mut Manager<'a>
}
可以看出
Interface
中的生命周期'a
来自于Manager
中的生命周期'a
Manager
中的生命周期'a
就是其中字符串切片text
的生命周期,如下图
同样的对于结构体 List 也是此逻辑
struct List<'a> {
manager: Manager<'a>,
}
根据上面的结论,再来分析问题代码段
impl<'a> List<'a> {
// 此处生命周期参数 'a 是编译错误的核心原因
pub fn get_interface(&'a mut self) -> Interface {
Interface {
manager: &mut self.manager
}
}
}
为 List<'a>
实现了方法,方法参数为 &'a mut self
,一个对 self 的可变引用,生命周期为 'a
,由于前面我们分析出 List
的 'a
实际上就是 Manager
中 text
的 'a
,此时我们可以得到一个非常重要的结论
get_interface
方法参数中对于self
的可变引用的生命周期就取决于self.manager.text
的生命周期
再来看 main 函数中的代码
fn main() {
let mut list = List {
manager: Manager {
text: "hello"
}
};
list.get_interface().noop();
println!("Interface should be dropped here and the borrow released");
// 下面的调用会失败,因为同时有不可变/可变借用
// 但是Interface在之前调用完成后就应该被释放了
use_list(&list);
}
按照一般逻辑,此处获取不可变引用,然后调用 noop 传入所有权,此行代码结束后,可变引用应该被释放才对
list.get_interface().noop();
但是由于 get_interface
生命周期的约束条件,编译器认为可变引用的生命周期还未结束,如下图
所以核心问题在于 可变引用活得太久了
,如何解决呢?
思路就是不让 list 的可变引用生命周期于 text 相关,给它一个单独的生命周期,同时表明单独生命周期于 list 生命周期参数的大小关系
struct Interface<'b, 'a: 'b> {
manager: &'b mut Manager<'a>
}
impl<'b, 'a: 'b> Interface<'b, 'a> {
pub fn noop(self) {
println!("interface consumed");
}
}
struct Manager<'a> {
text: &'a str
}
struct List<'a> {
manager: Manager<'a>,
}
impl<'a> List<'a> {
pub fn get_interface<'b>(&'b mut self) -> Interface<'b, 'a>
where 'a: 'b {
Interface {
manager: &mut self.manager
}
}
}
fn main() {
let mut list = List {
manager: Manager {
text: "hello"
}
};
list.get_interface().noop();
println!("Interface should be dropped here and the borrow released");
// 下面的调用可以通过,因为Interface的生命周期不需要跟list一样长
use_list(&list);
}
fn use_list(list: &List) {
println!("{}", list.manager.text);
}
评论区