Rust难点笔记 1. 生命周期 [UPD2. 24.2.24]

yyi
yyi
2024-02-22 / 0 评论 / 91 阅读 / 正在检测是否收录...
温馨提示:
本文最后更新于2024年02月24日,已超过260天没有更新,若内容或图片失效,请留言反馈。

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的生命周期一致

来看看例子

image-20240222144816922

第一个例子里是第一条规则,没填就分配独立的

image-20240222144847430

第二个例子同理,lvl不是引用,没有生命周期

image-20240222144917844

第三个例子,用到了一二两条,给输入参数放了个'a,而入参只有'a 一个生命周期

第四个例子还是第一条规则

image-20240222145008967

这个用到了一三,如果有&Self,会和&Self的生命周期一致

image-20240222145116372

第一个例子无法为输出推断生命周期,因为没有输入

第二个例子无法推断生命周期,因为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

评论 (0)

取消