Rust 学习笔记(21)-生命周期

参考章节《Rust 程序设计语言》第10.3章 生命周期确保引用有效

生命周期有什么用?答:避免出现悬垂引用问题,那么问题来了,什么是悬垂引用

1
引用的对象已经被释放,但指针依然指向它,这会导致程序引用非预期的数据。

先说说看完书中这一章我自己的理解

  1. 生命周期是给谁看的?答:编译器!
  2. 生命周期就好像一套规则,告诉编译器以这个规则给我检查代码是否满足要求

我们先来看这么一个例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {}", r);
}

这段代码不能通过编译,原因是 r 引用的值(x)在尝试使用之前就离开了作用域。
那么Rust是怎么知道的呢?答:这得益于Rust的借用检查器

我们来看看借用检查器都做了什么

还是上面的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
                          // ---------+
}   

书中这一段解释的非常清楚,所以我就抄过来了

这里将 r 的生命周期标记为 'a 并将 x 的生命周期标记为 'b。如你所见,内部的 'b 块要比外部的生命周期 'a 小得多。
在编译时,Rust 比较这两个生命周期的大小,并发现 r 拥有生命周期 'a,不过它引用了一个拥有生命周期 'b 的对象。
但因为生命周期 'b 比生命周期 'a 要小:被引用的对象比它的引用者存在的时间更短。所以程序被拒绝编译,

让我们来看一个没有产生悬垂引用且可以正确编译的例子

1
2
3
4
5
6
7
8
{
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {}", r); //   |       |
                          // --+       |
}                         // ----------+

这里 x 拥有生命周期 'b,比 'a 要大。这就意味着 r 可以引用 x:Rust 知道 r 中的引用在 x 有效的时候也总是有效的。

当借用检查器无法帮助我们分析检查时

某些情况下,借用检查器无法帮助我们分析代码,这时候就需要我们手动标记一下,好让借用检查器按照我们的规则进行检查

那么什么情况下借用检查器无法帮助我们呢?

  1. 函数采用引用做参数,并且返回该引用,请看如下代码
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
        /*
        这里你可以想象成
        let r = &x;
        r
        */
    } else {
        y
        /*
        这里你可以想象成
        let r = &y;
        r
        */
    }
}

借用检查器不知道传入的引用的具体生命周期,因为 xy 在哪定义的不清楚嘛
由于不清楚 xy 的生命周期,因此借用检查器就无法让它们和 r 的生命周期进行比较,所以这段代码不允许编译

好了,我们知道了原因,接下来看看如何解决它

函数中的泛型生命周期

我们可以通过一个叫作生命周期注解的东西,来手动标记一下,注意,它并不会改变生命周期,而是告诉借用检查器,我要求的生命周期

生命周期注解有着一个不太常见的语法:生命周期参数名称必须以撇号(')开头,其名称通常全是小写,类似于泛型其名称非常短。
'a 是大多数人默认使用的名称。生命周期参数注解位于引用的 & 之后,并有一个空格来将引用类型与生命周期注解分隔开。

下面的代码,修复了上面的问题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

这段代码,指明了 x 必须拥有生命周期 'a,并且 y 也必须拥有生命周期 'a,这也就意味着,xy 必须处在同一个生命周期
然后是返回值也指明了必须拥有生命周期 'a,也就意味着,返回值必须是 xy 中的一个
这样的话,当传入的 xy 不在同一个生命周期中时,编译将不被通过

深入理解生命周期

指定生命周期参数的正确方式依赖函数实现的具体功能。

书中这一小节也讲的非常清楚,所以我就直接抄过来了

例如,如果将 longest 函数的实现修改为总是返回第一个参数而不是最长的字符串 slice,就不需要为参数 y 指定一个生命周期。

例如,如下代码将能够编译:

1
2
3
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

在这个例子中,我们为参数 x 和返回值指定了生命周期参数 'a,不过没有为参数 y 指定,因为 y 的生命周期与参数 x 和返回值的生命周期没有任何关系。

当从函数返回一个引用,返回值的生命周期参数需要与一个参数的生命周期参数相匹配。

如果返回的引用没有指向任何一个参数,那么唯一的可能就是它指向一个函数内部创建的值,那么它将会是一个悬垂引用,因为它将会在函数结束时离开作用域。
如果你非要返回一个没有指向任何一个参数的引用,那么你需要指明 'static 生命周期,这个后面我们会讲到
如果你非要返回一个没有指向任何一个参数的引用,那么你必须返回一个具有静态生命周期的引用,光指明 'static 是没有用的,因为生命周期注解不改变实际的生命周期

结构体定义中的生命周期注解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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,
    };
}

这个注解意味着 ImportantExcerpt 的实例不能比其 part 字段中的引用存在的更久,所以 ImportantExcerpt 实例中的引用总是有效的。

这一节,书中我认为没有讲清楚,为什么注解字段后,实例就不能比字段存在的久了?
它们是怎么比较的,难道指明了生命周期注解后字段的生命周期就比实例的生命周期大?
那么如果没有显示指明字段生命周期,是不是字段的生命周期就比实例的生命周期小?

我是这么理解的,如果字段没有显示的指明生命周期,那么字段是属于实例的,它和实例属于同一个生命周期
如果显示指明了生命周期,则字段的生命周期就会套在实例的生命周期外面,这里就死记硬背

生命周期省略(Lifetime Elision)

关于生命周期省略,我个人是不建议省略写法的,第一是记规则比较麻烦,第二是省略了会造成代码不明确,不过感兴趣你可以参考这一节生命周期省略

在方法中定义生命周期

在方法中也可以定义生命周期,只需要在 impl 关键词和 类型 后面声明生命周期即可,因为这些生命周期是结构体类型的一部分

1
2
3
4
5
impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

静态生命周期

所有的字符串字面值都是 'static 的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 返回一个静态生命周期的字符串 slice
fn hello() -> &'static str {
    // 'static,其生命周期能够存活于整个程序期间。所有的字符串字面值都拥有 'static 生命周期,我们也可以选择像下面这样标注出来
    // 这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的。
    let hello: &'static str = "Hello World.";
    hello
}

fn main() {
    let hello = hello();
    println!("{}", hello)
}
updatedupdated2024-10-012024-10-01