关于reborrow的一个复杂的例子
在查找关于Rust的reborrow的语法时,发现这么一篇文章Stuff the Identity Function Does (in Rust)。然后……看不懂,《Programming Rust》快看完了,这篇文章还是看不懂。
但是有很多不懂之处的文章,往往是最值得读的,因为它提供了一个线索,能把遗漏的知识串连起来,这是很难得的。
还好有Google, 一路搜索过来,大体也搞清楚了。
例子
文章里例子是这样的。有一个递归的数据结构,List:
1 | struct List { |
写一个函数来遍历它
1 | impl List { |
可以在Rust的playgroud里测试一下。你会发现这段代码是通不过编译的。
问题在哪呢?
实际上这短短一段代码使用了很多隐晦的语法。
先来搞清楚它在做什么吧。
match的是什么?
在Rust的match表达式的分支里,是可以用&
来匹配reference的,比如:
1 | struct Foo { |
(注: 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 | fn walk_the_list(mut self) { |
编译器会告诉你
1 | error[E0507]: cannot move out of borrowed content |
错误信息和一些细节
如果你在试着编译最上边关于List的代码(也可以playgroud里执行run),编译器会报两个error。
1 | error[E0499]: cannot borrow `current.next.0` as mutable more than once at a time |
下边来搞清楚这两个错是怎么造成的。首先明确一下move和borrow这两个概念是否可用于reference,以及(如果可用的话)有什么作用。
move和borrow
首先要搞清楚被move和borrow的是什么?
move会造成两个结果,一个是被move的值之前所在的内存变成了uninitialized状态,二是把之前的值拷贝到了move的目标内存。
所以,不仅像let a = 1
里边的a
这样的非reference的值可以被move, reference本身也可以被move。
像下边这个例子:
1 | struct Foo { |
编译器会说use of moved value: b
,这是说b
作为一个reference,它的值被move了,b
成了未初始化状态,编译器会确保后边对b
的使用(除非再给b赋值)会报错。
那么,reference可以被borrow吗?也是可以的:
1 | struct Foo { |
这一段编译器报的错是:
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 | trait DerefMut: Deref { |
所以,由于需一个&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 | fn walk_the_list(&mut self) { |
这里编译器会报错说:
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 | fn walk_the_list(&mut self, other: List) { |
编译器会说:
1 | error[E0506]: cannot assign to `*current` because it is borrowed |
当然,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.0
在inner
的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 | fn walk_the_list(&mut self) { |
就可以通过编译了。显然,是current = inner
这一句导致了在loop的一次迭代结束后,之前的current.next.0
仍然处于mutable borrowed状态。
而去掉current = inner
以后,inner
作为对current.next.0
的borrow的生命周期结束了,同时使得对current
的隐式的borrow结束了。
比如,下边这段代码会报错:
1 | fn walk_the_list(&mut self) { |
编译器说
1 | error[E0499]: cannot borrow `current.next` as mutable more than once at a time |
虽然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 | fn id<T>(x: T) -> T { x } |
这次编译器就不会再报多次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 | fn walk_the_list(&mut self) { |
这里显式地把current
给 move到了z
,使得后边的current = innner
可以成功。编译器就可以进行同样的推导。
而且,正如原文中提到的,可以用Rust的一种特殊的语法 – {}
, 来进行move。
1 | fn walk_the_list_with_braces(&mut self) { |
现在想一下这段代码的作用,它只是walk the list, 在方法执行完以后,这个List还跟原来一样。所以,如果只是实现同样的功能,下边的代码就够了:
1 | fn walk_the_list(&self) { |
原文在最后提了一下consume这个list的方法
1 | impl List { |
这里在consume_the_list_with_braes
里{self}
貌似没有必要,但是如果你去掉{}
,编译器会说:
1 | error[E0596]: cannot borrow immutable argument `self` as mutable |
由于是self
而非mut self
,self
是一个immutable binding, 是不能对它进行 mutable borrow的。
但是通过{}
,self
的值被move出来到了一个临时的值中,没有对它的任何immutable binding,这样就绕过了immutable binding的限制。