关于reborrow的一个复杂的例子

在查找关于Rust的reborrow的语法时,发现这么一篇文章Stuff the Identity Function Does (in Rust)。然后……看不懂,《Programming Rust》快看完了,这篇文章还是看不懂。
但是有很多不懂之处的文章,往往是最值得读的,因为它提供了一个线索,能把遗漏的知识串连起来,这是很难得的。

还好有Google, 一路搜索过来,大体也搞清楚了。

例子

文章里例子是这样的。有一个递归的数据结构,List:

1
2
3
struct List {
next: Option<Box<List>>,
}

写一个函数来遍历它

1
2
3
4
5
6
7
8
9
10
11
impl List {
fn walk_the_list(&mut self) {
let mut current = self;
loop {
match current.next {
None => return,
Some(ref mut inner) => current = inner,
}
}
}
}

可以在Rust的playgroud里测试一下。你会发现这段代码是通不过编译的。
问题在哪呢?

实际上这短短一段代码使用了很多隐晦的语法。

先来搞清楚它在做什么吧。

match的是什么?

在Rust的match表达式的分支里,是可以用&来匹配reference的,比如:

1
2
3
4
5
6
7
8
9
struct Foo {
v: i32
}

let mut a = Foo{v: 1};
match &a {
&Foo{v} => println!("{}", v + 1),
_ => panic!()
}

(注: Rust 1.26里有了更简化的写法,见Announcing Rust 1.26里的”Nicer match bindings
“)

但是在List的例子里, 不是这种情况, current.next并非一个reference。
所以,这个match表达式绝非在match一个reference,而是一个类型为<Option<Box<List>>的值。

ref 关键字

some(ref mut inner)这句以前貌似没见过。查了一下,发现这是在match表达式的分支里专用的一个语法。
可以参照这篇文章:& vs. ref in Rust patterns。ref用于把一个原来需要move的地方,
改成只获取reference,从而避免move。在前边的代码里,实际上就是想避免对current.next的move。
实际上,由于current是一个reference,是不能通过它把current.next的值move out出去的。

比如, 下边的代码试着把current.next move给d:

1
2
3
4
fn walk_the_list(mut self) {
let mut current = &mut self;
let d = current.next;
}

编译器会告诉你

1
2
3
4
5
6
7
8
error[E0507]: cannot move out of borrowed content
--> src/main.rs:54:21
|
54 | let d = current.next;
| ^^^^^^^-----
| |
| cannot move out of borrowed content
| help: consider using a reference instead: `&current.next`

错误信息和一些细节

如果你在试着编译最上边关于List的代码(也可以playgroud里执行run),编译器会报两个error。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
error[E0499]: cannot borrow `current.next.0` as mutable more than once at a time
--> src/main.rs:19:26
|
19 | Some(ref mut inner) => current = inner,
| ^^^^^^^^^^^^^ mutable borrow starts here in previous iteration of loop
...
22 | }
| - mutable borrow ends here

error[E0506]: cannot assign to `current` because it is borrowed
--> src/main.rs:19:44
|
19 | Some(ref mut inner) => current = inner,
| ------------- ^^^^^^^^^^^^^^^ assignment to borrowed `current` occurs here
| |
| borrow of `current` occurs here

下边来搞清楚这两个错是怎么造成的。首先明确一下move和borrow这两个概念是否可用于reference,以及(如果可用的话)有什么作用。

move和borrow

首先要搞清楚被move和borrow的是什么?
move会造成两个结果,一个是被move的值之前所在的内存变成了uninitialized状态,二是把之前的值拷贝到了move的目标内存。
所以,不仅像let a = 1里边的a这样的非reference的值可以被move, reference本身也可以被move。
像下边这个例子:

1
2
3
4
5
6
7
8
struct Foo {
v: i32
}

let mut a = Foo{v:1};
let b = &mut a;
let c = b;
b;

编译器会说use of moved value: b,这是说b作为一个reference,它的值被move了,b成了未初始化状态,编译器会确保后边对b的使用(除非再给b赋值)会报错。

那么,reference可以被borrow吗?也是可以的:

1
2
3
4
5
6
7
8
struct Foo {
v: i32
}

let mut a = Foo{v:1};
let mut b = &mut a;
let c = &mut b;
let d = &mut b;

这一段编译器报的错是:

cannot borrow b as mutable more than once at a time

这就跟List的例子里编译器报的错很相似了。

cannot borrow current.next.0 as mutable more than once at a time

DerefMut

这里说同时不能borrow current.next.0超过一次。这个borrow是发生在Some(ref mut inner)这个branch里。但是current.next.0是个什么鬼?
而且current = inner这两边的类型不一致呀。 inner的类型是&mut Box<List>, 而current的类型是&mut List
这里是有一个隐式的转换的,把&mut Box<List>转成了&mut List。这应该是Box实现了Defmut这个trait。实际上也是如此,Box实现了这个trait

1
impl<T: ?Sized> DerefMut for Box<T>

而DerefMut的定义是这样的

1
2
3
trait DerefMut: Deref {
fn deref_mut(&mut self) -> &mut Self::Target;
}

所以,由于需一个&mut List,但是目前只有&mut Box<List>
所以,Rust编译器会调用Box::deref_mut(inner),返回对Box内的List的mutable reference。
好了,我们了解了inner的类型为&mut Box<List>,它是对next为Some时其中的Box<List>的mutable reference。
它被赋值给current时发生了地deref_mut的调用(一个隐式类型转换)。

current是怎么被borrow的?

前边已知道的是,current.next.0是被borrow的状态。那么,为什么编译器同时也认为current也是在被borrow状态呢?
而且,在z = &mut current.next的例子中,borrow的是current.next, 编译器也是报错说current被borrow了多次。

auto-dereference

current.next很明显是说List里的叫next的field。但是current是一个reference呀,它没有next这个field呀。这里是用的Rust的auto-dereference
语法,实际上是调用的(*current).next。但是,还不只这么简单,看下边的例子。

通过current.next进行的reborrow

1
2
3
4
5
fn walk_the_list(&mut self) {
let mut current = self;
let d = &mut current.next;
let z = &mut current;
}

这里编译器会报错说:

error[E0499]: cannot borrow current as mutable more than once at a time
–> src/main.rs:37:26
|
36 | let d = &mut current.next;
| ———— first mutable borrow occurs here
37 | let z = &mut current;
| ^^^^^^^ second mutable borrow occurs here
38 | }
| - first borrow ends here

它不会允许我们再次borrow current这个reference了,因为我们通过&mut current.next隐式地使得current被borrow了。
注意,这是使得current这个reference被borrow了,而非current指向的值被borrow了。

不过,事实上*current也被borrow了。看这个例子

1
2
3
4
5
fn walk_the_list(&mut self, other: List) {
let mut current = self;
let b = &mut current.next;
*current = other;
}

编译器会说:

1
2
3
4
5
6
7
error[E0506]: cannot assign to `*current` because it is borrowed
--> src/main.rs:47:13
|
46 | let d = &mut current.next;
| ------------ borrow of `*current` occurs here
47 | *current = other;
| ^^^^^^^^^^^^^^^^ assignment to borrowed `*current` occurs here

当然,current就是*current的mutable reference。但是,编译器的这句话指向的是在let d = &mut current.next时, *current被borrow的。
这句话应该这么看,如果只有current这唯一的一个mutable reference,那么*current = other就是合理的。但是,let d = &mut current.next产生
了另一个我们拿不到的mutable reference,可以把&mut a.b可以认为实际上执行的是&mut (*a).b, 这样*a就被borrow了,这样在d的生命周期里,current
就不可用了。

也就是说,如果我们通过let c = &mut a.b的形式来搞到一个b的mutable reference时。编译器会使得a处于mutable borrowed状态, 无论a是不是reference。
而且假如b的owner path的更上游也会处于这个状态。

ownship path

关于reference的 ‘shared vs mutation’,《Programming Rust》里这么说:

Each kind of reference affects what we can do with the values along the owning path to the reference, and the values reachable from the reference.
Note that in both cases, the path of ownership leading to the referent cannot be changed for the references’s lifetime.For a shared borrow, the path is readonly; for a mutable borrow, it’s completely inaccessible. So there’s no way for the program to do anything that will invalidate the reference.

Rust这样做的目的,就是使刚才获得的mutable reference:c不会成为dangling pointer。当我们获取一个mutable reference时,例如inner, 它是对current.next.0的mutable borrow, 那么 the path of ownship leading to current.next.0inner的lifetime中都是’completely inaccessible’的。
这里 inaccessible 并非是说在以后的代码里就不能用current这个变量了,而是说不通单独使用它,例如let x = current
the path of ownship leading to the referent 是说inner所指向的值,也就是current.next.0的owner, 以及owner的owner … 。
由于当一个值有了mutable reference期间,只能通过这个reference使用它,所以可以认为mutable reference在这个ownship path上跟它的referent是处于同一位置。
所以current也就成了 inaccessible 的了。

loop

现在还是没有搞清楚为什么这个loop block会导致错误。

可以参见borrowing in loops,

首先,假如原代码改成

1
2
3
4
5
6
7
8
9
fn walk_the_list(&mut self) {
let mut current = self;
loop {
match current.next {
None => return,
Some(ref mut inner) => (),
}
}
}

就可以通过编译了。显然,是current = inner这一句导致了在loop的一次迭代结束后,之前的current.next.0仍然处于mutable borrowed状态。
而去掉current = inner以后,inner作为对current.next.0的borrow的生命周期结束了,同时使得对current的隐式的borrow结束了。
比如,下边这段代码会报错:

1
2
3
4
5
6
7
8
fn walk_the_list(&mut self) {
let mut current = self;
match current.next {
None => return,
Some(ref mut inner) => current = inner,
}
let ref_current = &mut current.next;
}

编译器说

1
2
3
4
5
6
7
8
9
10
error[E0499]: cannot borrow `current.next` as mutable more than once at a time
--> src/main.rs:65:36
|
63 | Some(ref mut inner) => current = inner,
| ------------- first mutable borrow occurs here
64 | }
65 | let ref_current = &mut current.next;
| ^^^^^^^^^^^^ second mutable borrow occurs here
66 | }
| - first borrow ends here

虽然current = inner这句不能通过编译,但是编译器仍然认为inner的生命周期在match结束后没有结束,这就使得current.next仍然处于被borrow的状态。
在原来的例子里,这就意味着current.next.0仍然处于被borrow的状态。同样由于current = inner不能执行,编译器认为两次迭代borrow的是同一个
current.next.0

你需要的只是move

那么怎么可以让它摆脱这种状态呢?编译器一直报怨的是多次borrow了current这个reference它的
所以只要在match前把它move出来就行了。因为当current再次被赋值时,就可以认为对它的borrow跟对上一个current的borrow完全没关系了,只不过被borrow的值的名字都叫current而已。
这也是move这个语法的本意,一个变量的值被move走之后,它就成了未初始化状态,就跟之前的值和之前的ownership path啥的没有关系了。
当它再被赋值后,它的属性就继承了对刚才被赋的值的属性,跟它的前世也没啥关系。

下边我们用上作者提到的id函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn id<T>(x: T) -> T { x }

fn walk_the_list(&mut self) {
let mut current = self;
match id(current).next {
None => return,
Some(ref mut inner) => current = inner,
}

match current.next {
None => return,
Some(ref mut inner) => current = inner,
}
}

这次编译器就不会再报多次borrow的错误了。因为id函数把current的值move给了函数参数。

这里要注意id的实现细节,它是一个generic function,如果参数的类型不是T,而是&mut List,即把id改成:

1
fn id(x: &mut List) -> &mut List { x }

就还会有同样的错误。
这就牵扯到了一个更隐晦的问题,就是Rust什么时候会move,什么时候会reborrow。
Why can I use an &mut reference twice?
里有所提及。其中的一个说法是,&Mut List并非current的全部类型,因为所有的reference的lifetime是它的类型的一部分,所以,当把current传给修改后的id时,
编译器会使得reborrow,而非move。但是使用泛型,就可以完全匹配current的类型。

事实上,还有别的方法可以通过编译,而且不用id函数,例如:

1
2
3
4
5
6
7
8
9
10
fn walk_the_list(&mut self) {
let mut current = self;
loop {
let mut tmp = current;
match tmp.next {
None => return,
Some(ref mut inner) => current = inner,
}
}
}

这里显式地把current给 move到了z,使得后边的current = innner可以成功。编译器就可以进行同样的推导。

而且,正如原文中提到的,可以用Rust的一种特殊的语法 – {}, 来进行move。

1
2
3
4
5
6
7
8
9
fn walk_the_list_with_braces(&mut self) {
let mut current = self;
loop {
match {current}.next {
None => return,
Some(ref mut inner) => current = inner,
}
}
}

现在想一下这段代码的作用,它只是walk the list, 在方法执行完以后,这个List还跟原来一样。所以,如果只是实现同样的功能,下边的代码就够了:

1
2
3
4
5
6
7
8
9
fn walk_the_list(&self) {
let mut current = self;
loop {
match current.next {
None => return,
Some(ref inner) => current = inner,
}
}
}

原文在最后提了一下consume这个list的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
impl List {
fn walk_the_list_with_braces(&mut self) {
let mut current = self;
loop {
match {current}.next {
None => return,
Some(ref mut inner) => current = inner,
}
}
}

fn consume_the_list_with_braces(self) {
{self}.walk_the_list_with_braces();
}
}

这里在consume_the_list_with_braes{self}貌似没有必要,但是如果你去掉{},编译器会说:

1
2
3
4
5
6
7
8
9
error[E0596]: cannot borrow immutable argument `self` as mutable
--> src/main.rs:45:13
|
44 | fn consume_the_list_with_braces(self) {
| ---- consider changing this to `mut self`
45 | self.walk_the_list_with_braces();
| ^^^^ cannot borrow mutably

error: aborting due to previous error

由于是self而非mut selfself是一个immutable binding, 是不能对它进行 mutable borrow的。
但是通过{}self的值被move出来到了一个临时的值中,没有对它的任何immutable binding,这样就绕过了immutable binding的限制。