问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

Rust生命周期详解:定义、意义与泛型生命周期的应用

创作时间:
作者:
@小白创作中心

Rust生命周期详解:定义、意义与泛型生命周期的应用

引用
CSDN
1.
https://blog.csdn.net/weixin_71793197/article/details/144938554

Rust的生命周期是该语言最独特且最具挑战性的特性之一。它确保了引用的有效性,防止了悬空引用的发生。本文将从生命周期的基本概念出发,通过具体示例,深入讲解其在函数中的应用,帮助读者掌握这一核心概念。

10.5.1. 什么是生命周期

Rust的每个引用都有自己的生命周期,生命周期的作用是让引用保持有效,也可以说它是保持引用有效的作用域。在大多数情况下,生命周期是隐式的、可推断的。如果引用的生命周期可能以不同的方式相互关联时,就必须手动地标注生命周期。生命周期可以说是Rust与其它语言相比最与众不同的特征,所以它非常难学。

10.5.2. 生命周期的存在意义

生命周期存在的主要目的是为了避免悬空引用(Dangling reference,又叫悬垂引用),这个概念在4.4. 引用与借用中有讲过,我把当时对悬空引用的解释粘到这来:

在使用指针时非常容易引起叫做悬空指针(Dangling Pointer)的错误,其定义为:一个指针引用了内存中的某个地址,而这块内存可能已经释放并分配给其他人使用了如果你引用了某些数据,Rust编译器保证在引用离开作用域前数据不会离开作用域。这是Rust保证悬空引用永远不会出现的做法。

看个例子:

fn main() {
    let r;
    { //小花括号
        let x = 5;
        r = &x;
    }
    println!("{}", r);
}
  • 在这个例子中先声明了 r,但是没有初始化,这么做的目的是让 r 存在于小花括号外(看代码的注释的位置)的作用域。当然,Rust没有 Null 值,所以在 r 初始化前不能使用 r
  • 而在小花括号内声明了变量 x,赋值为5。下边一行把 x 的引用赋给了 r
  • 当小花括号这个作用域结束之后,在它外面又打印了 r 这段代码是有错误的,错误在于当打印 r 时, x 已经走出作用域被销毁了。所以 r 的值,也就是 x 的引用此时指向的内存地址是已经被释放的内存,指向的数据已经不是 x 了,造成了悬空引用,所以会报错。

输出:

error[E0597]: `x` does not live long enough
 --> src/main.rs:5:7
  |
4 |         let x = 5;
  |             - binding `x` declared here
5 |         r = &x;
  |             ^^ borrowed value does not live long enough
6 |     }
  |     - `x` dropped here while still borrowed
7 |     println!("{}", r);
  |                    - borrow later used here

报错信息是借用的值活的时间不够长。因为在内部花括号结束的时候 x 走出作用域,但 r 作用域更大能够继续使用,为了保证程序的安全性,这个时候任何基于 r 的操作都是无法正确运行的。Rust会通过借用检查器来检查代码的合法性。

10.5.3. 借用检查器(borrow tracker)

借用检查器会比较作用域来判断所有的借用是否合法。对于刚才那个代码例,借用检查器发现 r 的值是 x 的引用,但是 r 的生命周期比 x 长,就会报错。怎么解决这个问题呢?很简单,让 x 的生命周期不小于 r 就行:

fn main() {
    let x = 5;
    let r = &x;
    println!("{}", r);
}

这个时候 x 的生命周期是从第2行到第5行, r 的生命周期是从第3行到第5行,所以 x 的生命周期就完全覆盖了 r 的生命周期,程序就不会报错。

10.5.4. 函数中的泛型生命周期

看个例子:

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

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

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
  • string1 这个变量是 String 类型,而 string2 的类型是字符串切片 &str,然后把这两个值传进 longest 函数( string1 需要先转化一下成 &str 类型),把得到的返回值打印出来。
  • longest 函数的逻辑是把输入的两个参数做对比,选更长的那个返回

输出:

error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

错误是缺少生命周期的标注,具体地说是返回类型缺少生命周期参数。看下面的 help,说这个函数的返回类型包含了一个借用的值,但是函数的签名没有说明这个借用的值是来自 x 还是来自 y,考虑引入一个命名的生命周期参数。

看回这个函数:

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

很明显,这个函数的返回值要么是 x 要么是 y,但具体是哪个不一定,而 xy 这两个传入的参数的具体生命周期也是不知道的(只看这个函数的情况下)。所以没法像之前的例子那样比较作用域,从而判断返回的引用是否是一直有效的。借用检查器也做不到,原因就是它不知道这个返回类型的生命周期到底是跟 x 有关还是跟 y 有关。

实际上就算返回值是确定的这么写也会报错:

fn longest(x: &str, y: &str) -> &str {
    x
}

输出:

error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

编译器还是不清楚,因为函数类型体现不出来返回类型借用的值是来自 x 还是来自 y

所以这个事跟函数体里的逻辑没有关系,就是跟函数签名有关系,那该怎么改呢?我们其实可以按照报错信息里的帮助提示来改:

= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

它让我们加个泛型生命周期参数我们就加:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

'a 表示有 a 这么一个生命周期, xy 以及返回类型都是这个生命周期 a,这个时候就表示 xy 和返回类型的生命周期是一样的。“一样的”这个说法不太准确,因为 xymain 函数对应的实例的生命周期其实有一点差别,但这个内容下篇文章再讲。

先看看代码整体:

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

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

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

输出:

The longest string is abcd
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号