rust基础入门[22] - Borrowing and Lifetimes

本章覆盖有:

  • “租借,borrowing”和“生命周期,lifetime”的概念
  • 困扰系统软件的租借典型错误是哪些
  • 如何通过租借checker,Rust的严格语法来避免这种典型错误
  • 如何通过插入语句块来约束租借的作用域(scope)
  • 为什么函数返回的引用需要生命周期指示符(specifiers)
  • 如何给函数使用生命周期指示符(lifetime specifiers),它们表示什么
  • 租借checker的任务是什么

Ownership and Borrowing

上一章介绍到,当将变量a赋值给b时,会有两种情况:如果它们类型是可拷贝的(copyable),它就实现了Copy特质(当然也肯定实现了Clone);如果它们的类型不可拷贝(non-copyable),则没有实现Copy(Clone可能实现,也可能没有)。

第一种情况,用到拷贝语义(copy semantics)。意味着,在赋值过程中,当a保留它对象的所有权(ownership),一个新的对象被创建,初始化值等同于a的值,以及b获得这些新的对象的所有权。当ab离开它的作用域时,它们拥有的对象被销毁(又叫dropped)。

相反,第二种情况,用到移动语义(move semantics)。意味着,在赋值过程中,a将它的所有权移交给了b,不会有新对象的创建,a不再可访问。当b离开它的作用域时,它拥有的对象被销毁。当a离开它的作用域,不发生任何事情。

所有这些保证了合适的内存管理,只要没有引用被使用到。

但看看这个代码,

1
2
let n = 12;
let ref_to_n = &n;

第一条语句后,变量n拥有一个数。

第二条语句后,变量ref_to_n拥有一个引用,该引用指向同一个由n引用的数。它是一个所有权吗?

它不能作为一个所有权,因为这个数早已经由n所拥有,如果同时被这个引用所“拥有”,它将会被销毁两次。因此,类似这样的引用不能“拥有”对象。

表达式n*ref_to_n指向同一个对象,但仅n拥有这个对象。变量ref_to_n可以访问这个对象,但不是“拥有”这个对象。这种概念称为租借,borrowing。我们说ref_to_n借了n拥有的数。这种租借,开始于引用指向该对象,结束于该对象的销毁。

关于可变性(mutability),有两种类型的borrowing:

1
2
3
let mut n = 12;
let ref1_to_n = &mut n;
let ref2_to_n = &n;

这段程序中,ref1_to_nn拥有的值,租借为 mutably 的值,以及ref2_to_n租借为 immutably 的值。第一种是可变租借(mutable borrowing),第二种是不可变租借(immutable borrowing)。可变租借仅能从可变变量中获取。

Object Lifetimes

注意到,“作用域,scope”的概念作用于编译期的变量,而不是运行期的对象。对应运行期对象的概念叫“生命周期,lifetime”。在Rust中,一个对象的生命周期,指的是一系列执行指令,从执行指令的创建,到执行指令的销毁。在该时间段,该对象叫做“存活,to live,to be alive”。

当然,作用域和生命周期存在一定关系,但它们不是同一个概念。例如:

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

该程序中,变量a的作用域开始于第一行,而a拥有的对象的生命周期开始于第二行。通常认为,变量a的作用域开始于它的声明,对象的生命周期开始于该对象接收一个值。

即使是变量作用域(scope)的结束,跟对象生命周期(lifetime)的结束也不是同时发生的,

1
2
3
4
5
6
let mut a = "Hello".to_string();
let mut b = a;
print!("{}, ", b);
a = "world".to_string();
print!("{}!", a);
b = a;

结果将输出:“Hello, world!”。

在第一条语句,变量a被声明以及初始化。因此a的作用域开始,接着a拥有的对象被创建,a的生命周期开始。

在第二条语句,变量b被声明,由a移动对象进行初始化。因此,b的作用域开始,a的作用域被悬挂(suspended),因为它被移动了,所以它不可再被访问。b拥有的对象不用创建,因为它就是先前创建的对象。

在第三条语句,b被访问。

在第四条语句,变量a通过new构造器,指派新的值。这里,a恢复(resume)它的作用域(scope),因为它的作用域还没有结束。一个新的对象被创建,该对象的生命周期开始。前面由于变量a被“移动”了,所以它不“拥有”任何对象。所以这里的语句类似于一个初始化。

在第五条语句,a(拥有对象)可被访问了。

在第六条语句,a再次被移动到b,它的作用域再次被悬挂(suspended)。相反,b一直是活动的(active),它拥有的对象由移动的a替换,因此,原先的对象在这里被销毁,以及生命周期结束。如果该对象实现了Drop,在这里,它的drop方法会被调用。

最后,ba陆续退出它们的作用域。变量b拥有一个对象,该对象被销毁,以及结束它的生命周期。相反,变量a被“移动”了,不再拥有任何对象,也就不会有销毁对象的发生。

Errors Regarding Borrowing

C和C++程序编写总是被各种错误困扰,而Rust则通过设计来避免这一类问题。Rust的一种常见错误是“use after move”,前面介绍过。另一种错误如下,

1
2
3
4
5
6
7
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的引用进行初始化,它租借(borrow)了这个对象。

接着,变量ref_to_n指向的对象,即值为12的对象,打印输出。

接着,语句块结束,内部变量n结束了它的作用域,它的对象被销毁。

接着,变量ref_to_n指向的对象再次被打印。但该对象原先被n“拥有”,它现在不存在了!

幸运的是,Rust编译器拒绝该代码,产生错误信息“n does not live long enough”。该消息表示,变量n死了(dying),但仍然有指向它“拥有”的对象的引用,它应该活更长一些;至少应该跟租借它对象的租借方一样长。

顺便,C或C++对应的代码如下,

1
2
3
4
5
6
7
8
9
10
11
#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错误,

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

对应的C语言实现是,

1
2
3
4
5
6
7
8
9
10
11
#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++的实现是,

1
2
3
4
5
6
7
8
#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编译器会拒绝并抛出错误信息“cannot borrow v as mutable because it is also borrowed as immutable”。让我们看看该程序有什么错误。

首先,可变(mutable)变量v,用一个仅带数12的vector声明和初始化。

然后,变量ref_to_first用原先指向v的引用声明和初始化。引用指向的值包含数12。

然后,另一个数13被添加到vector。但这种插入会导致缓冲区的重新分配。即使在这个例子,ref_to_first仍会继续指向旧的对象,不是有效的内存地址。

最后,这个旧的内存地址,可能是错误的,读取的内存地址的值打印后,是不可预测的结果。

由于从一个vector插入或添加元素时,所有指向vector的引用都“失效”,导致这种错误的出现。通常,这种错误属于广义的错误类别,也就是一个数据结构,通过几种路径、或别名访问,当数据结构被其中一个别名修改时,不能被另一个别名使用。

我们将这类编程错误命名为“use after change by an alias”。

How to Prevent “Use After Drop” Errors

Rust防止使用“脱落,dropped”对象的技术是简单的。

把该对象看做是被一个变量指向,遵循栈分配的规则,会按照变量声明的反向顺序被脱落,而不是初始化的反向顺序。

1
2
3
4
5
6
7
8
9
10
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”,因此回收以及脱落的按相反的顺序。

为了避免使用脱落对象,所有变量,租借其它变量拥有的对象,必须在该变量的 后面 声明。

例如,

1
2
3
4
5
let n = 12;
let mut _r;
let m = 13;
_r = &m;
_r = &n;

这段代码会产生错误信息:“m does not live long enough”。这是因为_r同时从mn进行borrow,虽然不在同一时刻指向两者,但它在m之前声明了。要更正这段代码,改为如下,

1
2
3
4
5
let n = 12;
let m = 13;
let mut _r;
_r = &m;
_r = &n;

这段代码是合法的,当mn拥有的对象被dropped时,不会再有指向它们的引用。

How to Prevent “Use After Change by an Alias” Errors

由于其它变量引起对象的改变,导致当前变量使用该对象出现错误。要避免这种错误,使用的规则有几分复杂。

首先,要求考虑任何语句会读这个对象,不会有写操作,就像是该对象的一个 临时不可变租借(temporary immutable borrowing);任何语句变更这个对象,就像是该对象的一个 临时可变租借(temporary mutable borrowing)。这种租借的出现和结束在该语句的内部进行。

然后,租借开始于,获取指向该对象的引用,并分配给一个变量;结束于,该变量的作用域(scope)结束。

下面是一个例子,

1
2
3
4
5
6
7
8
9
10
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”。

在第3行和最后一行,一个可变租借开始并结束。在第5行,一个不可变租借开始,第6行,一个可变租借开始;它们在语句块末尾结束。在第9行,一个可变租借开始并结束。

这种规则要求,同一时刻,可变租借(即,&mut )的出现不能和其它租借并存。

换句话说,同一时刻:

  • 没有租借
  • 或,一个单一的可变(mutable)租借
  • 或,一个单一的不可变(immutable)租借
  • 或,几个不可变(immutable)租借

不能有:

  • 几个可变(mutable)租借
  • 不能有一个单一的可变(mutable)租借和一个或多个不可变(immutable)租借

Listing the Possible Cases of Multiple Borrowings

下面罗列六种允许的情况。

第一种:

1
2
3
let a = 12;
let _b = &a;
let _c = &a;

有两个不可变租借,直到结束为止被持有。

第二种:

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

第三种:

1
2
3
let mut a = 12;
a = 13;
let _b = &a;

一个不可变租借,紧随后一个临时可变租借。

第四种:

1
2
3
let mut a = 12;
a = 13;
let _b = &mut a;

一个可变租借,紧随后一个临时可变租借。

第五种:

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

一个不可变租借,紧随后一个临时不可变租借。

第六种:

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

一个可变租借,紧随后一个临时不可租借。

下面是六种不合法情况。

第一种:

1
2
3
let mut a = 12;
let _b = &mut a;
let _c = &a;

编译出错“cannot borrow a as immutable because it is also borrowed as mutable”。

第二种:

1
2
3
let mut a = 12;
let _b = &a;
let _c = &mut a;

编译出错“cannot borrow a as mutable because it is also borrowed as immutable”。

第三种:

1
2
3
let mut a = 12;
let _b = &mut a;
let _c = &mut a;

编译出错“cannot borrow a as mutable more than once at a time”。

第四种:

1
2
3
let mut a = 12;
let _b = &a;
a = 13;

编译出错“cannot assign to a because it is borrowed”。

第五种:

1
2
3
let mut a = 12;
let _b = &mut a;
a = 13;

编译出错“cannot assign to a because it is borrowed”。

第六种:

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

编译出错“cannot borrow a as immutable because it is also borrowed as mutable”。

梳理一下。对于当前尚未被borrowed的对象来说,允许的操作是:

  1. 仅可被不可变租借(immutablely borrowed)数次,然后可以由所有者(owner)和任何租借方(borrower)读取。
  2. 仅可被可变租借(mutably borrowed)一次,然后有且仅能由这个租借方(borrower)读取和修改。

Using a Bock to Restrict Borrowing Scope

当一个对象的租借结束后,对象对其它租借又变得可用了。任何类型的租借都可限制在语句块内。

1
2
3
4
5
6
7
let mut a = 12;
{
let b = &mut a;
*b += 1;
}
let c = &mut a;
*c += 2;

这是允许的,因为租借发生在第三行,语句块结束后,租借也结束了,因此第7行的a可以用于其它租借。

类似地,对于函数也一样,在函数结束后,对象再次变得可用。

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

这种规则,适用于Rust确保可以自动地决定内存的回收,避免不合法的引用;这种规则允许Rust实现无数据竞争(data-race-free)的并发编程。

The Need of Lifetime Specifiers for Returned References

先看这段代码:

1
2
3
4
5
6
7
8
9
10
11
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]”。

变量v1v2各自拥有vector。接着被两个引用borrowed了,分别被_x1_x2拥有。因此,在第7行后,_x1租借了变量v1拥有的vector,_x2租借了变量v2拥有的vector。这是被允许的,因为_x1v1之后声明,_x2v2之后声明,这些引用比它们租借对象存活时间短。

在第八行,有个简单的表达式_x1。因为它是语句块的最后表达式,该表达式的值成为语句的值,因此该值被用来初始化变量result。该值是一个指向v1的引用,result变量租借了vector。这是被允许的,因为result的声明在v1之后,因此可以租借v1拥有的对象。

现在,小小的改动:在第八行替换"1"和"2"。

1
2
3
4
5
6
7
8
9
10
11
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 does not live long enough”。因为result的值来自于_x2表达式,由于_x2租借了v2的对象,所以result租借了vector。因为resultv2之前声明,因此不能租借它的对象。

Rust编译器致力于这种推理的部分称作“租借检查,borrow checker”。

现在,让我们尝试转换前面两个代码,将内部语句转换为一个函数。第一个变为,

1
2
3
4
5
6
7
8
9
10
let v1 = vec![118, 22];
let result;
{
let v2 = vec![33u8];
fn func(_x1: &Vec<u8>, _x2: &Vec<u8>) -> &Vec<u8> {
_x1
}
result = func(&v1, &v2);
}
print!("{:?}", *result);

第二个变为,

1
2
3
4
5
6
7
8
9
10
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函数本身是合法的。但borrow checker会查找它们不兼容的具体用法。

泛型函数的参数边界使用traits,函数调用是否有效,依据该函数体的内容,这不是好事。主要是因为不能从错误信息理解出错的原因,除非你清楚函数体的内部代码。另一个原因是,函数被调用时,如果该函数体内任意调用了其它函数,确保main函数是合法的,borrow checker需要分析程序的所有函数。这种整个程序的分析带来了过度的复杂。

因此,类似于泛型函数,返回一个引用的函数,必须在函数签名阈将borrow-checking隔离!函数的borrow-check,仅考虑签名,函数体,函数调用的签名,而不需要考虑函数体调用其它函数。

因此,前面的两个程序都会出现编译错误:“missing lifetime specifier”。“声明周期指示器,lifetime specifier”是一个函数签名的一个装饰,它允许borrow checker对函数体和函数调用分离检查。

不管怎样,函数或语句体的租借行为应该是“到此为止”,不应该再将它进一步传递给其它函数或表达式,以避免带来不必要的副作用,未知的错误信息。

Usage and Meaning of Lifetime Specifiers

讨论到函数的调用和生命周期,下面是一个简单例子。

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

在任何Rust函数内,仅可以参考的有:

  • 函数参数拥有的对象(v1拥有的vector);
  • 本地变量拥有的对象(s拥有的动态字符串);
  • 临时对象(动态字符串表达式"Hello".to_string());
  • 静态对象(字符串字面量"Hello");
  • 函数参数租借的对象,由预先存在的某些变量拥有,发生在当前函数调用处(v2租借过来的对象)。

当一个函数返回一个引用,这个引用的对象不能推断是由函数参数拥有,还是由本地变量拥有,还是由临时对象拥有,因为当函数return时,本地变量,函数参数,临时变量都会被销毁。因此,这个引用将被悬挂。

相反,这个引用却可以推断得知是静态对象,或是函数参数租借的对象。

下面是这两种情况的中第一种情况的示例(尽管代码在Rust不被允许),

1
2
3
fn func() -> &str {
"Hello"
}
1
2
3
fn func(v: &Vec<u8>) -> &u8 {
&v[3]
}

borrow checker仅对返回值的引用感兴趣,该引用有两种:指向静态对象,或接收的参数的租借对象。borrow checker需要知道哪些引用是指向静态对象,哪些是租借的参数对象;如果有几个参数租借对象,需要知道它们中哪些租借了非静态返回的引用。

不带lifetime specifer的函数签名是不合法的,

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

函数签名参数中有两个引用,返回值类型也包含有两个引用。后两个引用可能指向静态对象,或者参数b的租借对象,或者参数c的第二个字段的租借对象。

下面表达式指定了一种可能的情况,

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

和泛型函数用法一样,在函数名后面,添加一个参数列表。如其说是一个类型参数,实际上它是一个生命周期指示器(lifetime specifier)。

从句<'a>是一个声明。它表示:<<In this function signature, a lifetime specifier is used; its name is "a">>。名字"a"是任意的。它简单表示所有出现的情况,“匹配”这种出现。它类似泛型函数的类型参数,不同的地方在于,生命周期指示器前缀使用单引号,另外,按照约定,泛型参数首字母使用大写,生命周期指示器使用一个小写字母,如a,b,c,…。

然后,函数签名包含其它三个'a生命周期指示器的出现,b参数的类型、c参数的第二字段的类型、返回值的第一个字段的类型。相反,返回值的第三个字段类型用了static生命周期标识。

a生命周期指示器的用法表示:“返回值的第一个字段租借已存在的b参数和c参数第二个字段的对象,因此它存活比该对象要短”。

相反,static生命周期指示器的用法表示:“返回值的第三个字段指向一个静态对象,因此它可以存活在任何时间,甚至和整个进程一样长”。

当然,这仅是一种可能生命周期注解,下面是另一种,

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

c参数不带注解,引用的对象不租借给返回值的引用,

还有另一种可能生命周期注解,

1
2
3
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的第二个字段的对象。另外,函数有两个类型参数T1T2,不带trait边界。

Checking the Validity of Lifetime Specifiers

编译任何函数时,borrow checker有两个工作:

  • 通过函数自身和函数体,检查函数签名是否有效。
  • 检查函数体是否合法,统计该函数被调用次数。

本小节,先看第一种情况。

如果函数返回值没有引用类型,borrow checker不做任何处理。

否则,每个返回值引用,必须检查是否有正确的生命周期指示器(lifetime specifier)。

这样一个指示器(specifier)可以是“'static”。这种情况,引用必须指向一个静态对象。

1
2
3
4
5
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”。该程序是合法的,因为三个返回值的引用都是静态对象。

相反,下面

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

将会生成编译错误:“lifetime of reference outlives lifetime of borrowed content…”。这是不合法的,返回值不是一个指向静态对象的引用;它实际上是接收参数的值,该返回值租借了函数参数引用对象的同一个对象。

生命周期指示器可以定义成列表的形式,如下

1
2
3
4
5
6
7
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 true 12”。这是有效的,因为tuple中返回的第一个字段引用的时y表达式的值,以y参数和返回值的第一个字段有同样的生命周期指示器(lifetime specifier);它们的生命周期指示器都是'b。返回值的第三个字段和参数x一样有同一个生命周期指示器'a

相反,下面

1
2
3
4
5
6
7
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”。实际上,返回值的第一个字段和第三个字段都有生命周期指示器,但不是对应它们函数签名。

注意到,多个返回字段可能仅用了一个生命周期指示器:

1
2
3
4
5
6
7
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。但味道完全不一样。

原来的版本是,参数列表指示了会有两个独立的生命周期;这个版本,它们共享生命周期。

这个改动对于租借检查器(borrow checker)来说不简单。让我们考虑一个更复杂的函数体:

1
2
3
4
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的对象。返回值有和参数等同的生命周期,所以满足borrow checker。

相反,下面

1
2
3
4
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,这个参数没有生命周期指示器,所以该代码是不合法的。

即使下面代码也是不合法的,

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

当处理数据流分析时,编译器会探测到y从不被返回值租借;但borrow checker坚持因为&y[0]是一个可能返回值,所以该段程序被认为是无效的。

Using the Lifetime Specifiers of Invoked Functions

上一章节的开始部分说过,borrow checker有两项工作,编译函数时进行检查函数体是否有效,统计函数体内任意被调用函数的签名。

沿用上一节的代码示例。根据租借的规则,“missing lifetime specifier”表示缺少lifetime specifier,我们将原来的加上生命周期指示器,

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

第二个例子改为,

1
2
3
4
5
6
7
8
9
10
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 does not live long enough”。

为什么func这些写法会产生编译错误,前面小节已经解析过了。

让我们看看main函数在第一个程序如何工作的。当func被调用,存活v1resultv2,按顺序声明,以及v1v2早已被初始化。func的签名说结果值和第一个参数有相同的lifetime specifier,这意味着result的值不会存活得比v1长。以及,resultv1之后声明,因此它会先于v1销毁。

再看看为什么第二段程序的main函数是不合法的。这里,func的签名说返回值和第二个参数有相同的lifetime specifier,意味着result存活不会比v2长。但resultv2之前声明,而它会在其后被销毁。

这是因为只使用了一个lifetime specifiter,下面改为两个指示器,

1
2
3
4
5
6
7
8
9
10
11
fn f<'a, 'b>(x: &'a i32, y: &'b i32) -> (&'b 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);

结果将打印“12 true 13”。

相反,如果仅用一个lifetime specifier时,是不合法的,

1
2
3
4
5
6
7
8
9
10
11
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 does not live long enough”。

在这两个版本中,函数f都会接收i1j1的引用,首先返回变量r存储的值,然后分别地初始化i2j2变量。

在第一个版本中,第一个参数和返回值的第一个字段拥有相同的lifetime specifier,这导致了i2必须存活少于i1。类似地,j2必须存活少于j1。实际上,变量的声明顺序需要满足这些要求。

第二个版本,由于只有一个lifetime specifier,所以i2j2必须存活少于i1j1。实际上,i2被声明在j1前,不能满足这些要求。