Beginng_Rust(译):借用和生命周期(第二十二章)

在本章中,您将学习:

•“借用”和“生命周期”的概念

•哪些是关于借用的典型编程错误,即困扰系统软件

•Rust严格语法如何使用借用检查器来防止此类典型错误

•插入块的方式如何限制借用范围

•为什么函数返回引用需要生命周期指示符

•如何使用寿命指定符来表示函数及其含义

•借阅检查器的任务是什么

借用和生命周期

我们已经看到,当你将变量“a”赋给变量“b”时,有两种情况:它们的类型是可复制的,也就是说,它实现了“复制”特性(它确实实现了“克隆”特性) ,也); 或者它们的类型不可复制,即它不实现“复制”特征(它可能实现克隆特征,也可能不实现)。

在第一种情况下,使用复制语义。 这意味着,在该赋值中,当“a”保持其对象的所有权时,创建一个新对象,最初等于“a”表示的对象,“b”获得该新对象的所有权。 当“a”和“b”超出其范围时,它们拥有的对象将被销毁(也称为“丢弃”)。

相反,在第二种情况下,使用移动语义。 这意味着,在该赋值中,“a”将其对象的所有权移交给“b”,不创建任何对象,并且不再可访问“a”。 当“b”超出其范围时,其拥有的对象将被销毁。 当“a”超出其范围时,没有任何反应。

所有这些都保证了内存的正确管理,只要不使用引用即可。
但是看看这个有效的代码:

let n = 12;
let ref_to_n = &n;

在第一个语句之后,“n”变量拥有一个数字。

在第二个语句之后,“ref_to_n”变量拥有一个引用,该引用引用“n”引用的相同数字。 这是否是所有权?

它不能是所有权,因为该数字已经由“n”拥有,如果它也由该引用拥有,它将被销毁两次。 所以,像这样的引用永远不会拥有一个对象。

“n”和“* ref_to_n”表达式引用相同的对象,但只有“n”变量拥有它。 “ref_to_n”变量可以访问该对象,但它不拥有它。 这样的概念被称为“借用”。 我们说“ref_to_n”“借用”“n”所拥有的相同数字。 这种借用在引用开始引用该对象时开始,并在引用被销毁时结束。

关于可变性,有两种借用方式:

let mut n = 12;
let ref1_to_n = &mut n;
let ref2_to_n = &n;

在这个程序中,“ref1_to_n”变量可变地借用“n”所拥有的数字,而“ref2_to_n”变量则不可取地借用该对象。 第一个是可变借款,而第二个是不可改变的借款。 当然,您只能从可变变量中获取可变借用。

对象生命周期

请注意,“作用域”的概念适用于编译时变量,而不适用于运行时对象。 关于运行时对象的相对概念被命名为“生命周期”。 在Rust中,对象的生命周期是创建它的指令执行和销毁它的指令执行之间的指令执行序列。 在这段时间内,该对象被称为“活着”或“活着”。

当然,范围和寿命之间存在关系,但它们不是相同的概念。 例如:

let a;
a = 12;
print!("{}", a);

在这个程序中,“a”变量的范围从第一行开始,而“a”所拥有的对象的生命周期从第二行开始。 通常,变量的范围在声明该变量时开始,并且当该对象接收到值时,对象的生命周期开始。

即使变量范围的结尾也不总是与其拥有对象的生命周期结束的点重合。

let mut a = "Hello".to_string();
let mut b = a;
print!("{}, ", b);
a = "world".to_string();
print!("{}!", a);
b = a;

这将打印:“你好,世界!”。

在第一个语句中,声明并初始化“a”变量。 因此,“a”的范围开始,并且“a”拥有的对象被创建,因此其生命周期开始。

在第二个语句中,通过移动“a”变量来声明和初始化“b”变量。 因此,“b”的范围开始,并且“a”的范围被暂停,因为它被移动,因此不再可访问。 不创建“b”所拥有的对象,因为它与前一个语句中创建的对象相同。

在第三个语句中,访问“b”(及其拥有的对象)。 在此声明中访问“a”是非法的。

在第四个语句中,通过创建新字符串为“a”变量分配新值。 在这里,“a”恢复其范围,但尚未结束。 创建了一个新对象,因此它的生命周期从这里开始。 “a”变量被“移动”,因此它没有任何对象。 所以这个语句类似于初始化。

在第五个语句中,访问“a”(及其拥有的对象)。

在第六个语句中,“a”再次移动到“b”,因此其范围再次被暂停。 相反,“b”处于活动状态并保持活动状态。 “b”所拥有的对象被从“a”移动的对象所取代,因此前一个对象在这里被销毁,并且它的生命周期结束。 如果该对象的类型实现了“Drop”特征,那么它的“drop”方法将在此语句中在先前由“b”拥有的对象上调用。

在程序结束时,首先“b”然后“a”退出它们的范围。 “b”变量拥有一个对象(包含“world”字符串),因此该对象现在被销毁,因此终止了它的生命周期。 相反,“a”变量被“移动”,因此它没有任何东西,因此没有其他东西被破坏。

关于借用的错误

用C和C ++编写的程序受到Rust在设计中避免的错误的困扰。 一个是前一章中所见的“移动后使用”错误。 另一个例子如下:

let ref_to_n;
{
let n = 12;
ref_to_n = &n;
print!("{} ", *ref_to_n);
}
print!("{}", *ref_to_n);

首先,声明“ref_to_n”变量,但不进行初始化。 然后,在内部块中,声明并初始化“n”可变变量,因此它在堆栈中分配一个数字,其值为12。

然后,前一个变量通过引用“n”变量进行初始化,因此它借用了该对象。

然后,打印由值为12的“ref_to_n”变量引用的对象。

然后,内部块结束,因此内部变量“n”结束其范围,因此其对象被破坏。

然后,再次打印由“ref_to_n”变量引用的对象。 但是这样的对象是“n”变量拥有的对象,现在不再存在了! 幸运的是,Rust编译器拒绝此代码,在关闭块的大括号中发出错误消息“n不能长时间运行”。 该消息意味着“n”变量正在死亡,但仍有一些对它所拥有的对象的引用,因此它应该活得更久; 它应该至少与它所拥有的物品的所有借用人一样长寿。

顺便说一句,相应的C和C ++程序是这样的:

#include <stdio.h>
int main() {
int* ref_to_n;
{
int n = 12;
ref_to_n = &n;
printf("%d ", *ref_to_n);
}
printf("%d", *ref_to_n);
return 0;
}

C和C ++编译器接受这样的程序。 生成的程序打印“12”,然后以不可预测的方式运行(尽管通常打印另一个数字)。

让我们命名为“use after drop”这种编程错误。

但Rust还有另一种错误,例如以下程序:

let mut v = vec![12];
let ref_to_first = &v[0];
v.push(13);
print!("{}", ref_to_first);

C语言中的相应程序是:

#include <stdio.h>
#include <stdlib.h>
int main() {
int* v = malloc(1 * sizeof (int));
v[0] = 12;
const int* ref_to_first = &v[0];
v = realloc(v, 2 * sizeof (int));
v[1] = 13;
printf("%d", *ref_to_first);
free(v);
}

在C ++中它是:

#include <iostream>
#include <vector>
int main() {
std::vector<int> v { 12 };
const int& ref_to_first = v[0];
v.push_back(13);
std::cout << ref_to_first;
}

毋庸置疑,后两个程序被各自的编译器接受,即使它们的行为未定义。 相反,Rust编译器拒绝第一个程序,发出错误消息“不能借用v作为可变因为它也被借用为不可变的”。 让我们看看这个程序有什么问题。

首先,使用仅包含数字12的向量对象声明和初始化“v”可变变量。

然后,声明“ref_to_first”变量并使用对第一个“v”项的引用进行初始化。 因此,它是对包含数字12的对象的引用。

然后,向矢量添加另一个数字,其值为“13”。 但是这样的插入可能导致在包含向量项的缓冲区的另一个位置重新分配。 即使在这种情况下,“ref_to_first”变量仍将继续引用旧的,不再有效的内存位置。

最后,读取旧的,可能是错误的内存位置并打印其值,结果不可预测。

此错误是由于向向量中插入项目或从向量中删除项目“使”对该向量的所有引用“无效”这一事实引起的。 通常,此错误属于更广泛的错误类别,其中数据结构可通过多个路径或别名访问,并且当使用一个别名更改数据结构时,其他别名无法正确使用它。

让我们命名为“用别名改变之后使用”这种编程错误。

如何防止“Use After Drop”错误

Rust使用的技术来防止使用已经丢弃的对象很简单。

只需考虑遵循堆栈分配标准的变量直接引用的对象将按照变量声明的相反顺序删除,而不是按照初始化的相反顺序。

struct X(char);
impl Drop for X {
fn drop(&mut self) {
print!("{}", self.0);
}
}
let _a = X('a');
let _b;
let _c = X('c');
b = X('b');

该程序将打印“cba”。 这三个对象按“acb”的顺序构造,但拥有它们的三个变量按“abc”的顺序分配,因此deallocations和drop遵循其相反的顺序。

为避免使用被删除的对象,必须在该变量之后声明需要借用另一个变量所拥有的对象的所有变量。 检查此代码:

let n = 12;
let mut _r;
let m = 13;
_r = &m;
_r = &n;

此代码生成错误消息:“m的活动时间不够长”。 这是因为“_r”从“m”和“n”都借用,虽然它不是同时引用两者,而是在“m”之前声明。 要拥有正确的程序,您应该删除第四行,或交换第二行和第三行。

let n = 12;
let m = 13;
let mut _r;
_r = &m;
_r = &n;

这是有效的,因为当“n”和“m”拥有的对象被删除时,不再有对它们的引用。

如何防止“别名更改后使用”错误

避免使用通过另一个变量更改的对象的规则稍微复杂一些。

首先,需要考虑任何读取对象的语句,而不是将其写入,例如对该对象的临时不可变借用,以及任何更改对象的语句,如对该对象的临时可变借用。 此类借款在该声明中开始和结束。

然后,需要记住,每当引用一个对象并将其赋值给一个变量时,借用就开始了; 借款在这种变量范围的最后结束。
这是一个例子:

let a = 12;
let mut b = 13;
print!("{} ", a);
{
let c = &a;
let d = &mut b;
print!("{} {} ", c, d);
}
b += 1;
print!("{}", b);

这将打印:“12 12 13 14”。

无论是在第三行还是最后一行,都是不可改变的借贷开始和结束。 在第五行,不可改变的借款开始,在第六行,可变借用的开始; 他们都在第八行的封闭式支架处结束。 在第九行,可变借款的开始和结束。

然后,规则就是任何对象,在代码的任何一点,都不能同时具有可变借用和其他借用。

换句话说,它可以有:

•没有借用
•或单一的可变借用
•或单个不可变借用
•或几个不可变借用

但它不能有:
•几笔可变借用

•也不是单一的可变借用和一种或多种不可变借用

列出多次借用的可能案例

以下是所有六个允许的案例。
第一种情况:

let a = 12;
let _b = &a;
let _c = &a;

有两个不可改变的借款,这些借款一直持续到最后。
第二种情况:

let mut a = 12;
let _b = &a;
print!("{}", a);

当不可变借款持有时,存在不可变借款,随后是临时不可换借款。
第三种情况:

let mut a = 12;
a = 13;
let _b = &a;

当第一笔借款已经完成时,存在临时可变借款,随后是不可变借款。
第四种情况:

let mut a = 12;
a = 13;
let _b = &mut a;

当第一笔借款已经完成时,存在临时可变借款,随后是可变借款。
第五种情况:

let mut a = 12;
print!("{}", a);
let _b = &a;

当第一笔借款已经完成时,存在临时不可换借款,随后是不可变借款。
第六种情况:

let mut a = 12;
print!("{}", a);
let _b = &mut a;

当第一笔借款已经完成时,存在临时不可变借款,随后是可变借款。
这是所有六个非法案件。
第一种情况:

let mut a = 12;
let _b = &mut a;
let _c = &a;

有一个可变借款和一个不可改变的借款,这两个借款都会持续到最后。编译器发出错误消息:“不能将a借用为不可变的,因为它也被借用为可变的”。
第二种情况:

let mut a = 12;
let _b = &a;
let _c = &mut a;

有一个不可改变的借款和一个可变的借款,两者都会持续到最后。编译器发出错误消息:“不能借用a作为可变因为它也被借用为不可变的”。
第三种情况:

let mut a = 12;
let _b = &mut a;
let _c = &mut a;

有两个可变借款,两者都会持续到最后。编译器发出错误消息:“不能一次多次使用a作为可变的”。
第四种情况:

let mut a = 12;
let _b = &a;
a = 13;

存在不可改变的借款,随后是临时可变借款。编译器发出错误消息:“无法分配给’a`,因为它是借用的”。
第五种情况:

let mut a = 12;
let _b = &mut a;
a = 13;

有一个可变的借款,然后是暂时的可变借款。编译器发出错误消息:“无法分配给’a`,因为它是借用的”。
第六种情况:

let mut a = 12;
let _b = &mut a;
print!("{}", a);

有一个可变借款,然后是临时不可变借款。编译器发出错误消息:“不能将a借用为不可变的,因为它也被借用为可变的”。

说清楚,让我们以另一种方式重复相同的规则。 非当前借用的对象允许的唯一操作如下:

1.它只能通过不可改变的方式借用几次,然后只能由业主和任何借款人阅读。

2.它可以只被一次性地借来,然后只能通过这样的借款人来阅读或改变。

使用块来限制借用范围

借用对象时,该对象可用于其他借款。

我们已经看到临时借款持续一个单一的声明,但任何类型的借款都可以使用一个块来限制。

let mut a = 12;
{
let b = &mut a;
*b += 1;
}
let c = &mut a;
*c += 2;

这是允许的,因为从第三行开始的借用在第五行结束,因此在第七行“a”可用于另一次借用。

这在调用函数时非常典型。 前面的代码等同于下面的代码,其中块已在函数定义中转换并调用它:

let mut a = 12;
fn f(b: &mut i32) {
*b += 1;
}
f(&mut a);
let c = &mut a;
*c += 2;

当调用“f”函数时,可变借用从“a”所拥有的对象开始。 但是当函数结束时,无论出于何种原因,这种借用都会结束,因此可以通过“c”或其他函数调用来借用对象。

Rust采用这些规则来确保自动确定性内存释放,并避免无效引用; 但奇怪的是,由于其他原因,这些规则在计算机科学中已经众所周知。 “只允许一个作者或几个读者”是避免并发编程中所谓的“数据竞争”的规则。 因此,此规则还允许Rust具有无数据争用的并发编程。

此外,避免数据争用也会对单线程程序的性能产生很好的影响,因为它可以简化CPU缓存的一致性。

返回引用的生命周期说明符需求

现在看看这段代码:

let v1 = vec![11u8, 22];
let result;
{
let v2 = vec![33u8];
result = {
let _x1: &Vec<u8> = &v1;
let _x2: &Vec<u8> = &v2;
_x1
}
}
print!("{:?}", *result);

它将打印:“[11,22]”。

有两个向量,由变量“v1”和“v2”拥有。 然后,这些向量由两个引用借用,由变量“_x1”和“x2”拥有。 因此,在第七行之后,“ x1”正在借用“v1”所拥有的向量,而“_x2”正在借用“v2”所拥有的向量。 这是允许的,因为“_x1”在“v1”之后声明,而“_x2”在“v2”之后声明,因此这些引用比它们借用的对象少。

在第八行,有简单的表达式“_x1”。 因为它是块的最后一个表达式,所以这个表达式的值成为块本身的值,因此这个值用于初始化“result”变量。 这样的值是对“v1”所拥有的向量的引用,因此“result”变量也借用该向量。 这也是允许的,因为“result”是在“v1”之后声明的,因此它可以借用“v1”所拥有的对象。

现在,做一个微小的改变:在第八行中将“1”替换为“2”。

let v1 = vec![11u8, 22];
let result;
{
let v2 = vec![33u8];
result = {
let _x1: &Vec<u8> = &v1;
let _x2: &Vec<u8> = &v2;
_x2
}
}
print!("{:?}", *result);

这将产生编译错误:“v2的寿命不够长”。 发生这种情况是因为现在“result”从“_x2”表达式获取其值,并且“_x2”借用“v2”所拥有的向量,“result”也借用该向量。 但是“结果”在“v2”之前声明,因此它不能借用它的对象。

所有这些推理只是对我们已经看到的有关借贷的回顾,但它显示了借贷如何在几行中变得复杂。 顺便说一下,专门用于这种推理的Rust编译器的部分被命名为“借用检查器”。 我们刚看到借阅检查员有很多工作要做。

现在,让我们尝试通过在函数中封装最内层块中的代码来转换前两个程序。 第一个程序变成:

let v1 = vec![11u8, 22];
let result;
{
let v2 = vec![33u8];
fn func(_x1: &Vec<u8>, _x2: &Vec<u8>) -> &Vec<u8> {
_x1
}

result = func(&v1, &v2);
}
print!("{:?}", *result);

第二个程序变成:

let v1 = vec![11u8, 22];
let result;
{
let v2 = vec![33u8];
fn func(_x1: &Vec<u8>, _x2: &Vec<u8>) -> &Vec<u8> {
_x2
}
result = func(&v1, &v2);
}
print!("{:?}", *result);

它们之间的唯一区别是“func”函数的主体。

根据我们的规则到目前为止,第一个程序应该是有效的,第二个程序是非法的。 但是“func”函数的两个版本本身都是有效的。 只是借用检查器会发现它们与其特定用法不兼容。

正如我们已经看到使用traits的泛型函数参数边界一样,根据这样一个函数体的内容来考虑函数调用的有效或无效是一件坏事。 主要原因是错误消息只有那些知道函数体内代码的人才能理解。 另一个原因是,如果任何被调用函数的主体可以使调用函数的代码有效或无效,为了确保“main”函数有效,借用检查器应该分析程序的所有函数。 这样一个完整的程序分析将非常复杂。

因此,与泛型函数类似,返回引用的函数也必须在函数签名阈值处隔离借位检查。 在不考虑调用函数的主体的情况下,需要借用检查任何函数,仅考虑其签名,正文和任何调用函数的签名。

因此,以前的两个程序都会发出编译错误:“缺少生命周期说明符”。 “生命周期说明符”是函数签名的装饰,允许借用检查器单独检查该函数的主体,以及该函数的任何调用。

终身说明符的用法和意义

谈谈函数调用和生命周期,这是一个简单的示例函数。

fn func(v1: Vec<u32>, v2: &Vec<bool>) {
let s = "Hello".to_string();
}

在任何Rust函数中,您只能参考:

1.函数参数拥有的对象(如“v1”所拥有的向量);

2.局部变量拥有的对象(如“s”所拥有的动态字符串);

3.临时对象(如动态字符串表达式“Hello”.to_string());

4.静态对象(如字符串文字“Hello”);

5.由函数参数借用的对象,它们由预先存在当前函数调用的某个变量所拥有(如“v2”借用的向量)。

当函数返回引用时,这样的引用不能引用该函数的参数所拥有的对象(案例1),或者由该函数的局部变量(案例2)或临时对象(案例3)所拥有, 因为当函数返回时,每个局部变量,每个函数参数和每个临时对象都被销毁。 所以,这样的参考将是晃来晃去的。

相反,函数返回的引用可以引用静态对象(案例4),也可以引用函数参数借用的对象(案例5)。

下面是这两个最后一个案例中的第一个示例(虽然Rust不允许这个代码):

fn func() -> &str {
"Hello"
}

这是另一个案例的例子:

fn func(v: &Vec<u8>) -> &u8 {
&v[3]
}

因此,借用检查器只对返回值中包含的引用感兴趣,并且这样的引用可以有两种:引用静态对象,或者借用作为参数接收的一个对象。 为了在不分析函数体的情况下完成其工作,借用检查器需要知道哪些返回的引用引用静态对象,哪些借用作为参数接收的对象; 在第二种情况下,如果有几个对象作为参数被接收,那么它们中的哪一个被任何非静态返回的引用借用。

让我们看一个函数签名,没有生命周期说明符,因此是非法的:

trait Tr {
fn f(flag: bool, b: &i32, c: (char, &i32)) -> (&i32, f64, &i32);
}

此函数签名在其参数中有两个引用,在其返回值类型中也有两个引用。 最后两个引用中的每一个都可以引用一个静态对象,或者它可以借用已经由“b”参数借用的对象,或者它可以借用已经由“c”参数的第二个字段借用的对象。 以下是指定可能案例的语法:

trait Tr {
fn f<'a>(flag: bool, b: &'a i32, c: (char, &'a i32))
-> (&'a i32, f64, &'static i32);
}

在函数名称之后,添加了一个参数列表,就像用于泛型函数的参数列表一样。 但是,不是类型参数,而是有一个生命周期说明符。

“<'a>”子句只是一个声明。 这意味着:<<在此函数签名中,使用了生命周期说明符; 它的名字是“a”>>。 名称“a”是任意的。 它只是意味着在它出现的所有事件中,这种事件匹配。 它类似于泛型函数的类型参数,因此需要将生命周期说明符与类型参数区分开来。 带有前缀的单引号就是这样的区别。 此外,虽然按惯例类型参数以大写字母开头,但是生命周期说明符是单个小写字母,如“a”,“b”或“c”。

然后,此签名包含三个“a”生命周期说明符的出现,在“b”参数的类型中,在“c”参数类型的第二个字段中,以及在返回值的第一个字段中 类型。 相反,返回值类型的第三个字段由“'static”生存期说明符注释。

使用这样的“a”生命周期说明符意味着:“返回值的第一个字段借用了b参数和c参数的第二个字段已经借用的同一个对象,因此它必须比这个对象少。”

相反,使用“静态”生命周期说明符意味着:“返回值的第三个字段指的是静态对象,因此它可以在任何时间生存,甚至只要整个过程”。

当然,这只是一个可能的终身注释。 这是另一个:

trait Tr {
fn f<'a>(flag: bool, b: &'a i32, c: (char, &i32))
-> (&'static i32, f64, &'a i32);
}

在这种情况下,返回值的第一个字段具有静态生存期,这意味着它不会被限制为比其他对象更少生存。 相反,第三个字段具有与“b”参数相同的生命周期说明符,这意味着它应该比它更少,因为它借用了相同的对象。 “c”参数类型中的引用未注释,因为它引用的对象不会被返回值中的任何引用借用。

这是另一个可能的生命周期注释:

trait Tr {
fn f<'a, 'b, T1, T2>(flag: bool, b: &'a T1, c: (char, &'b i32))
-> (&'b i32, f64, &'a T2);
}

此通用函数有两个生命周期参数和两个类型参数。 生命周期参数“a”指定返回值的第三个字段借用已经由“b”参数借用的对象,而生命周期参数“b”指定返回值的第一个字段借用已经借用的对象。 “c”参数的第二个字段。 此外,该函数具有通常使用的两个类型参数“T1”和“T2”,这里没有特征界限。

检查终身说明符的有效性

我们说借用检查器在编译任何函数时有两个工作:

•检查该功能的签名本身及其正文是否有效。

•检查该函数的主体是否有效,同时考虑到正文中调用的任何函数的签名。

在本节中,我们将看到第一个这样的工作。

如果在函数返回值中没有引用,则借用检查器无需检查。

否则,对于返回值类型中包含的每个引用,它必须检查它是否具有适当的生存期说明符。

这样的说明符可以是“静态的”。 在这种情况下,这样的引用必须引用静态对象。

static FOUR: u8 = 4;
fn f() -> (bool, &'static u8, &'static str, &'static f64) {
(true, &FOUR, "Hello", &3.14)
}
print!("{} {} {} {}",
f().0, *f().1, f().2, *f().3);

它将打印:“true 4 Hello 3.14”。 这是有效的,因为返回的所有三个引用实际上都是静态对象。 相反,这个程序

fn f(n: &u8) -> &'static u8 {
n
}
print!("{}", *f(&12));

将生成编译错误:“引用的生命周期超过借用内容的生命周期…”。 这是非法的,因为返回的值不是对静态对象的引用; 它实际上是作为参数接收的相同值,因此这样的返回值借用与函数参数引用的对象相同的对象。

允许的另一个生命周期说明符是在参数列表中定义的,在函数名称后面。

fn f<'a, 'b>(x: &'a i32, y: &'b i32) -> (&'b i32, bool, &'a i32) {
(y, true, x)
}
let i = 12;
let j = 13;
let r = f(&i, &j);
print!("{} {} {}", *r.0, r.1, *r.2);

它将打印:“13真12”。 这是有效的,因为作为元组的第一个字段返回的引用是“y”表达式的值,并且y参数具有与返回值的第一个字段相同的生存期说明符; 它们都是“b”。 并且相同的对应关系适用于返回值的第三个字段和“x”参数; 它们都具有“a”寿命规格。
相反,这个程序

fn f<'a, 'b>(x: &'a i32, y: &'b i32) -> (&'b i32, bool, &'a i32) {
(x, true, y)
}
let i = 12;
let j = 13;
let r = f(&i, &j);
print!("{} {} {}", *r.0, r.1, *r.2);

将生成两个编译错误,两者都带有错误消息:“lifetime mismatch”。 实际上,返回值的第一个和第三个字段都具有在参数列表中指定的生命周期,该生命周期与返回值类型中指定的生命周期不同。
请注意,可以为多个返回值字段使用一个生命周期说明符:

fn f<'a>(x: &'a i32, y: &'a i32) -> (&'a i32, bool, &'a i32) {
(x, true, y)
}
let i = 12;
let j = 13;
let r = f(&i, &j);
print!("{} {} {}", *r.0, r.1, *r.2);

这里,“b”寿命说明符已被“a”替换。 但是,此解决方案与前一个解决方案具有不同的含义。

在前面的解决方案中,参数列表中包含的两个引用具有独立的生命周期; 相反,在最后一个解决方案中,它们共享相同的生命周期。

借用检查员的工作并不总是那么容易。 让我们考虑一个更复杂的函数体:

fn f<'a>(n: i32, x: &'a Vec<u8>, y: &Vec<u8>) -> &'a u8 {
if n == 0 { return &x[0]; }
if n < 0 { &x[1] } else { &x[2] }
}

此功能有效。 在正文中有三种可能的表达式返回函数的值,并且所有这些表达式都借用了由“x”参数借用的相同对象。 这样的参数具有与返回值相同的生命周期,因此借用检查器得到满足。
相反,在这个功能中,

fn f<'a>(n: i32, x: &'a Vec<u8>, y: &Vec<u8>) -> &'a u8 {
if n == 0 { return &x[0]; }
if n < 0 { &x[1] } else { &y[2] }
}

一个可能的返回值,表达式“&y [2]”的值,借用“y”借用的对象,并且这样的参数没有生命周期说明符,因此这段代码是非法的。
即使这段代码也是非法的:

fn f<'a>(x: &'a Vec<u8>, y: &Vec<u8>) -> &'a u8 {
if true { &x[0] } else { &y[0] }
}

在执行数据流分析时,编译器可以检测到“y”从未被此函数的返回值借用; 但是借用检查器坚持认为“&y [0]”是一个可能的返回值,因此它将此代码视为无效。

使用调用函数的生命周期说明符

正如我们在上一节开头所说的那样,借用检查器的两个工作之一是在编译函数时检查该函数的主体是否有效,同时考虑到在函数体中调用的任何函数的签名。

例如,回到“返回引用的生命周期指示符需求”部分的最后两个程序。 我们说,根据我们的借款规则,第一个程序应该是有效的,第二个程序是非法的; 但是,我们得到了两个程序的“缺少生命周期说明符”。 以下是这两个程序,添加了适当的生命周期说明符。
这是第一个:

let v1 = vec![11u8, 22];
let result;
{
let v2 = vec![33u8];
fn func<'a>(_x1: &'a Vec<u8>, _x2: &Vec<u8>) -> &'a Vec<u8> {
_x1
}
result = func(&v1, &v2);
}
print!("{:?}", *result);

这是第二个:

let v1 = vec![11u8, 22];
let result;
{
let v2 = vec![33u8];
fn func<'a>(_x1: &Vec<u8>, _x2: &'a Vec<u8>) -> &'a Vec<u8> {
_x2
}
result = func(&v1, &v2);
}
print!("{:?}", *result);

第一个程序是有效的,它将打印“[11,22]”,而对于第二个程序,编译器将打印“v2的寿命不够长”。 两者都具有与原始程序完全相同的行为,它们不使用函数。

之前已经解释了以这些方式编写两个“func”函数的原因。

现在让我们看看第一个程序中的“主要”功能是如何工作的。 当调用“func”时,实时变量是“v1”,“result”和“v2”,按顺序声明,“v1”和“v2”已经初始化。 “func”的签名表示结果值具有与第一个参数相同的生存期说明符,这意味着分配给“result”的值必须不超过“v1”。 实际上这是成立的,因为“结果”已在“v1”之后声明,因此它将在它之前被销毁。

最后,让我们看看为什么第二个程序中的“主要”功能是非法的。 这里,“func”的签名表示结果值具有与第二个参数相同的生存期说明符,这意味着分配给“result”的值必须不超过“v2”。 但这实际上并不成立,因为“结果”已在“v2”之前声明,因此它将在它之后被销毁。

现在让我们解释为什么在上一节的最后一个例子中,只使用一个生命周期说明符不如使用两个生命周期说明符来表示“f”函数。 该计划有效:

fn f<'a, 'b>(x: &'a i32, y: &'b i32) -> (&'a i32, bool, &'b i32) {
(x, true, y)
}
let i1 = 12;
let i2;
let j1 = 13;
let j2;
let r = f(&i1, &j1);
i2 = r.0;
j2 = r.2;
print!("{} {} {}", *i2, r.1, *j2);

它将打印“12真13”。

相反,这个程序,其中第一行中的“b”生命周期参数已被“a”替换,是非法的:

fn f<'a>(x: &'a i32, y: &'a i32) -> (&'a i32, bool, &'a i32) {
(x, true, y)
}
let i1 = 12;
let i2;
let j1 = 13;
let j2;
let r = f(&i1, &j1);
i2 = r.0;
j2 = r.2;
print!("{} {} {}", *i2, r.1, *j2);

它会生成编译错误:“j1的寿命不够长”。

在两个版本中,f函数接收对数字“i1”和“j1”的引用,返回的元组首先存储在“r”变量中,然后其第一个和第三个值用于初始化“i2”和“j2” “变量,分别。

在程序的第一个版本中,第一个参数和返回值的第一个字段具有相同的生命周期说明符,这导致“i2”必须小于“i1”。 同样,“j2”必须小于“j1”。 实际上,这些变量的声明顺序满足了这些要求。

在程序的第二个版本中,只有一个生命周期说明符,根据该说明符,“i2”和“j2”必须小于“i1”和“j1”。 实际上,“i2”在“j1”之前声明并且不满足这些要求。

猜你喜欢

转载自blog.csdn.net/m0_37696990/article/details/83152693