我的RUST学习—— 【第十章 10-3】生命周期与引用有效性

在第四章 “引用与借用” 中,我们漏了一个十分重要的定义:

RUST 中的每一个引用都有其 声明周期,也就是引用保持有效的作用域。

大多时刻生命周期是可以隐性推断的,但是也会出现引用的生命周期以一些不同方式相关联的情况,所以 Rust 需要我们使用泛型生命周期参数来注明他们的关系,这样就能确保运行时实际使用的引用绝对是有效的。

生命周期的概念不同于其他语言中类似的工具,这是 Rust 最与众不同的功能。

声明周期避免了垂悬引用

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

这个例子会报错:

error[E0597]: `x` does not live long enough
  --> src/main.rs:7:5
   |
6  |         r = &x;
   |              - borrow occurs here
7  |     }
   |     ^ `x` dropped here while still borrowed
...
10 | }
   | - borrowed value needs to live until here

其原因是 x 存活的不够久,x 的值在第七行就已经结束了,然而 r 依旧保持了对 x 内容的引用。这就是一个垂悬引用,这是不允许的。那么 Rust 是如何决定这段是否允许的呢?

借用检查器

Rust 编译器有一个 借用检查器(borrow checker),它比较作用域来确保所有的借用都是有效的。

我们把作用域理解为集合,这些集合的关系只有两种:

  • 子集
  • 并集

不可能出现交叉集。因此如果两个引用直接存在关联,一定是存在子集关系,即一个引用的作用域在另一个引用的作用域内部。因此,只需要比较作用域的有效长度即可。

在这里把 r 的生命周期 记为 a’,把 x 的生命周期记为 b’,很明显,后者的长度要小于前者。但是 a' 的 r 居然拥有了 b' 的 x 的引用,因此,编译器推断出—— 被引用的对象比它的引用者存在的时间更短。

因此,只有子集引用父集的时候是Ok的,短引长。比如下面这种就是可以的。

{
    let x = 5;            // ----------+-- 'b <- x
                          //           |
    let r = &x;           // --+-- 'a  | <- r
                          //   |       |
    println!("r: {}", r); //   |       |
                          // --+       |
}                         // ----------+

现在我们已经在一个具体的例子中展示了引用的生命周期位于何处,并讨论了 Rust 如何分析生命周期来保证引用总是有效的,接下来让我们聊聊在函数的上下文中参数和返回值的泛型生命周期。

函数中的泛型生命周期

我的理解:泛型声明周期并不是泛型的声明周期,而是指这个生命周期注解是泛化的,它的作用与泛型类似,都是用一个通用的注解来描述多个值的共同之处。

比如常说的泛型是指:“泛型类型参数”,泛型是泛型类型参数的缩写,原意是指泛化的。我在后面会使用“泛期”来替代泛型生命周期,以规避误解。

让我们来编写一个返回两个字符串 slice 中较长者的函数。这个函数获取两个字符串 slice 并返回一个字符串 slice。

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
    }
}

该代码目前是无法通过编译的,因为函数并不知道传入的值,也就无法判断,到底是返回x还是y,也就不知道返回值的生命周期与参数的生命周期的关系,因为入参的声明周期可能来自不同的作用域,从而形成 垂悬指针

生命周期注解语法

上一部分的最后,我说了,Rust 无法知道,返回值与参数的生命周期关系。所以这一节,我们要手动注明生命周期

生命周期注解 描述了多个引用生命周期相互的关系,它只是描述,并不影响真实的生命周期

注意,如果我没理解错,只有引用才需要有所谓的生命周期标注。

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

&i32        // 引用
&'a i32     // 带有显式生命周期的引用
&'a mut i32 // 带有显式生命周期的可变引用

单个的生命周期注解本身没有多少意义,因为生命周期注解是用来告诉 Rust 多个引用的泛型生命周期参数如何相互联系的。

函数签名中的生命周期注解

之前将的都是理论,这一节讲一下,具体的东西。

如果一个函数要添加“泛期”,需要先注明,这点和泛型一致。在函数名和参数列表之间,使用尖括号声明一个 泛期,与泛型位置一致。

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

凡是写在函数定义的东西,都是对入参和返回值的限制上面的代码的意思是说:函数有两个参数,一个返回值,并且两个参数的生命周期与返回值的生命周期必须一致。这种情况,你才能用我这个函数。

它的实际含义是 longest 函数返回的引用的生命周期与传入该函数的引用的生命周期的较小者一致。

为什么是较小者呢?因为代码在书写的时候肯定一行一行的,那么不同引用的生命周期肯定长短不一,但是它们又必须有相同的生命周期,因此要取交集

小例子

一个允许的例子

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

在这个例子中,string1 直到外部作用域结束都是有效的,string2 则在内部作用域中是有效的,而 result 则引用了一些直到内部作用域结束都是有效的值。借用检查器认可这些代码;它能够编译和运行,并打印出 The longest string is long string is long。

一个不允许的例子

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}
error[E0597]: `string2` does not live long enough
 --> src/main.rs:6:44
  |
6 |         result = longest(string1.as_str(), string2.as_str());
  |                                            ^^^^^^^ borrowed value does not live long enough
7 |     }
  |     - `string2` dropped here while still borrowed
8 |     println!("The longest string is {}", result);
  |                                          ------ borrow later used here

错误表明为了保证 println! 中的 result 是有效的,string2 需要直到外部作用域结束都是有效的。Rust 知道这些是因为(longest)函数的参数和返回值都使用了相同的生命周期参数 'a

如果从人的角度读上述代码,我们可能会觉得这个代码是正确的。 string1 更长,因此 result 会包含指向 string1 的引用。因为 string1 尚未离开作用域,对于 println! 来说 string1 的引用仍然是有效的。然而,我们通过生命周期参数告诉 Rust 的是: longest 函数返回的引用的生命周期应该与传入参数的生命周期中较短那个保持一致。因此,借用检查器不允许上面的代码,因为它可能会存在无效的引用。

深入理解生命周期

如果函数返回值的引用 没有 指向任何一个入参,那么唯一的可能就是,指向了函数内部创建的值,又由于函数内部创建的值会在函数结束后销毁,因此返回值指向的引用是一个垂悬引用。

我的理解:

  • 泛型和泛型生命周期容易搞混,后者称为泛生命周期更合适——泛期,前者全称是:泛型参数类型
  • 泛期和引用类型相关,如果是普通类型,没必要引入泛期
  • 泛期主要是帮助编译器判断,返回值的引用和入参在原调用上下文的关系(对函数)
  • 与返回值无关的参数没必要使用泛期

综上,生命周期语法是用于将函数的多个参数与其返回值的生命周期进行关联的。一旦他们形成了某种关联,Rust 就有了足够的信息来允许内存安全的操作并阻止会产生悬垂指针亦或是违反内存安全的行为。

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

上面讲了函数中的引用的生命周期注解,接下来讲讲结构体。

因为结构体不只是所有权结构体,结构体的成员也可能是外部值的引用。

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 用于存放字符串的slice,是一个引用。类似于泛型,泛期也要声明在结构体名之后,用尖括号包裹。

这个注解意味着,ImportantExcerpt 的实例的生命周期不能比 part 字段所引用的内容 存在更久。

可以这么理解,在此结构体里声明了泛期 a',表明有成员是引用类型,需要获得外部数据的引用,如果该结构体实例比外部数据存活的久,那么当外部数据被销毁后,该实例的成员就是一个悬垂引用。

生命周期省略

我们已经知道,每个引用都有一个生命周期,并且对于函数或结构体中的生命周期,我们要手动添加生命周期注解。

若是每一个函数或结构体都要手动添加注解,显得很麻烦,因此,Rust 团队把一些常见的、易于判断的模式编码进了Rust 编译器中。这样就可以自动推断。

被编码进 Rust 引用分析的模式被称为 生命周期省略规则(lifetime elision rules)。

函数或方法的参数的生命周期被称为 输入生命周期(input lifetimes),而返回值的生命周期被称为 输出生命周期(output lifetimes)。

编译器首先会进行规则推理,对于推理结束后仍存在模糊不清的引用,再进行报错。

三条规则,第一条规则适用于输入生命周期,后两条规则适用于输出生命周期。如果编译器检查完这三条规则后仍然存在没有计算出生命周期的引用,编译器将会停止并生成错误。这些规则适用于 fn 定义,以及 impl 块:

  • 每一个是引用的参数都有它自己独立的生命周期参数。
  • 如果只有一个输入生命周期参数,那么它也是所有输出生命周期参数。
  • 如果方法有多个输入生命周期参数并且其中一个参数是 &self&mut self,说明是个对象的方法(译者注: 这里涉及rust的面向对象参见17章), 那么所有输出生命周期参数被赋予 self 的生命周期。

例一:

fn first_word(s: &str) -> &str
规则1
fn first_word<'a> (s: &'a str) -> &str
规则2
fn first_word<'a> (s: &'a str) -> &'a str

该例适用于规则1、2,结果没有歧义,没问题。

例二:

fn longest(x: &str, y: &str) -> &str {
规则1
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'? str {

该例只适用于规则1,对于返回值的生命周期有歧义,因此报错。

方法定义中的生命周期注解

对有泛期的结构体定义方法的时候,也需要声明泛期。首先在 impl 之后声明有哪些泛期,之后不变。

struct ImportantExcerpt<'a> {
    part: &'a str,
}

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

一个应用声明周期规则的例子:

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

这里有两个输入生命周期,所以 Rust 应用第一条生命周期省略规则并给予 &selfannouncement 他们各自的生命周期。接着,因为其中一个参数是 &self,返回值类型被赋予了 &self 的生命周期,这样所有的生命周期都被计算出来了。

静态生命周期

这里有一种特殊的生命周期值得讨论:'static,其生命周期能够存活于整个程序期间。所有的字符串字面值都拥有 'static生命周期,我们也可以选择像下面这样标注出来:

let s: &'static str = "I have a static lifetime.";

这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的。因此所有的字符串字面值都是 'static 的。

你可能在错误信息的帮助文本中见过使用 'static 生命周期的建议,不过将引用指定为 'static 之前,思考一下这个引用是否真的在整个程序的生命周期里都有效。你也许要考虑是否希望它存在得这么久,即使这是可能的。大部分情况,代码中的问题是尝试创建一个悬垂引用或者可用的生命周期不匹配,请解决这些问题而不是指定一个 'static 的生命周期。

结合泛型类型参数、trait bounds 和生命周期

当遇到一个大杂烩时怎么写?

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
    where T: Display
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

总结

这一章介绍了很多的内容!现在你知道了泛型类型参数、trait 和 trait bounds 以及泛型生命周期类型,你已经准备好编写既不重复又能适用于多种场景的代码了。泛型类型参数意味着代码可以适用于不同的类型。trait 和 trait bounds 保证了即使类型是泛型的,这些类型也会拥有所需要的行为。由生命周期注解所指定的引用生命周期之间的关系保证了这些灵活多变的代码不会出现悬垂引用。而所有的这一切发生在编译时所以不会影响运行时效率!

你可能不会相信,这个话题还有更多需要学习的内容:第十七章会讨论 trait 对象,这是另一种使用 trait 的方式。第十九章会涉及到生命周期注解更复杂的场景,并讲解一些高级的类型系统功能。不过接下来,让我们聊聊如何在 Rust 中编写测试,来确保代码的所有功能能像我们希望的那样工作!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值