Rust难点笔记 1. 生命周期
说在最前面:这是一篇笔记,而不是教程,难免有不周到的、比较主观的想法,敬请大家批评指正和理解
这个系列是支撑我写rCore学习时边写边学产生的笔记,周期会比较长
远期计划会包括[生命周期, 宏, 异步, 智能指针, 链表]
01 生命周期
为什么要有生命周期? Rust的内存机制我们已经很熟悉了。生命周期和引用是强绑定的,就一句话:引用还在的时候,不能让引用的对象消失,从而造成悬垂指针问题
因此,我们在编译的时候如果出现一个被引用的对象在其引用前被销毁,就会报 xxx does not live so long的错误,这就是对象存活时间小于引用的生命周期,Rust不允许
02 生命周期标注
编译器有的时候没办法推断生命周期,需要我们手动标注元素的存活周期。
在了解生命周期标注的刚开始,最困扰我的问题就是:
- 我标注的东西,只是一个标注,真的能保证程序安全么
带着这个问题开始学
看一下最常见的生命周期例子
fn max(a: &i32, b: &i32) -> &i32 {
if *a > * b {
a
} else {
b
}
}
看到这个函数的第一个结论是:返回值的&i32必须来自于a或b,否则,任何max函数体内创建的变量都会在离开函数时销毁。
问题是编译器在编译时无法确定到底来自a还是b(程序员应该预期的是a和b都有可能),此时我们要声明a、b的返回值都是一样的(其实是,至少活的一样长,换句话说,我理解a是三者之间最短的那个的生命周期)
在函数后声明生命周期
fn max<'a>(a: &'a i32, b: &'a i32) -> &'a i32 {
if *a > * b {
a
} else {
b
}
}
此时,我们声明了该函数的两个参数应当和返回值生命周期一样长。
考虑如果不是max,是first
fn call_max(a: &i32) -> &i32 {
let b = 2;
max(a, &b)
}
fn max<'a>(a: &'a i32, b: &'a i32) -> &'a i32 {
a
}
此时我们只需要要求a的生命周期和返回值一样长了,但是因为标注了a、b都要符合生命周期,导致call_max报错
如果改为:
fn max<'a, 'b>(a: &'a i32, b: &'b i32) -> &'a i32 {
a
}
call_max就不再会报错了
回到报错的case,我们看看为什么
因为call_max中,原则上是不能返回b的引用的,b会在call_max退出时销毁。
由于调用的时候,a、&b和max的返回值遵循同一个生命周期,rust会取其中最小的,(也就是&b)
作为我们标注的生命周期
而作为call_max的返回值,其在caller处还可能会被用,但是由于标注最短的原则,它的生命周期只覆盖到call_max函数结尾,这肯定是不行的。
而把生命周期改为'b的时候,和'a解绑了,'a和传入call_max的a的生命周期一致,和call_max的返回值一致,而不依赖'b。所以可以通过
03 生命周期的函数省略
生命周期本来标的就乱七八糟的,编译器会在能推导的情况下生成默认的生命周期
https://doc.rust-lang.org/reference/lifetime-elision.html
文档里其实介绍了结构和特征的生命周期,但是目测是一法通万法通,先从函数开始
- 可省略的函数参数都有独立生命周期
- 所有参数共用生命后期,生命周期会被直接作用到返回值
- 如果是方法,返回值会和&self的生命周期一致
来看看例子
第一个例子里是第一条规则,没填就分配独立的
第二个例子同理,lvl不是引用,没有生命周期
第三个例子,用到了一二两条,给输入参数放了个'a,而入参只有'a 一个生命周期
第四个例子还是第一条规则
这个用到了一三,如果有&Self,会和&Self的生命周期一致
第一个例子无法为输出推断生命周期,因为没有输入
第二个例子无法推断生命周期,因为s和t的生命周期默认独立,不能推断返回值的
04 结构体生命周期
类比函数生命周期,给出一个例子就好懂了
struct ImportantExcerpt<'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 = ImportantExcerpt {
part: first_sentence,
};
}
这个例子说明part必须和持有它的结构体活的至少一样久
而这个代码就过不了
#[derive(Debug)]
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let i;
{
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
i = ImportantExcerpt {
part: first_sentence,
};
}
println!("{:?}",i);
}
因为结构体本身活到了println处,而part引用只能活到前一个作用域
05 方法生命周期
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
注意,这里ImportantExcerpt的完整签名是ImportantExcerpt<'a>,生命周期声明也是结构体的一部分
所以这里语法类似泛型
06 几个不太聪明的例子
例子来自course.rs
Rust的生命周期面对某些特定情况,可能不够聪明,比如:
#[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);
}
传入的虽然是可变借用,但是返回时实际上是一个不可变借用,所以理论上来说最终是不可变的
share处又进行了一次不可变借用,语义上说,两个不可变借用是没问题的,但是显然编译器不会那么听我的话
mut函数的生命周期标注实际上是这样的:
fn mutate_and_share<'a>(&'a mut self) -> &'a Self {
&'a *self
}
代表返回值loan和foo的可变借用 &mut self 生命周期相同,所以loan存续期间, &mut self也一直存续
虽然本身是个安全的东西,但是现在还没有机制支持,只能按我们标注的情况判定为不合法了。
再来看一个
#![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()
}
}
}
}
第一个map.get_mut(&key) 调用完成后,map的可变借用就可以结束了,但是因为我们的声明,这个借用一直存续到函数结束
因此map.insert() 会报错,因为产生了另一个可变借用。
评论 (0)