rust Reborrow and Borrow
五月 13, 2024 [C/C++] #Rust #obsidian #编译链接rust Reborrow and Borrow
考虑函数调用f(x),其中f: fn (&mut i32) -> i32, x: &mut i32,按照通常的逻辑,x将会被move到f的作用域中,最后在f返回时被丢弃,这样一来将无法第二次调用f(x)。然而实际上,可以无限次调用f(x),而仍保持x有效。
这是由于编译器会自动将f(x)转变为f(&mut *x),而&mut *x是所谓的reborrow操作,将会对mutable referencex再次borrow,获得一个新的mutable reference,而原本的x暂时被shadow,处于inactive状态,直到f返回时,新的mutable reference被销毁,x重新回到active状态。
对于generic函数,则不一定会进行reborrow操作,设v: &mut i32:
fn f<T>(x: &mut T):f(v)会自动reborrow,变成f(&mut *v)fn f<T>(x: T):f(v)不会自动reborrow,v会被move到f中fn f<T>(x: T, y: T): 对于f(v1, v2),只有v1会被move到f中,v2会被自动reborrow,即最后变成f(v1, &mut *v2)
所谓的reborrow实际上本质上仍是borrow,也就是说Rust并没有对&*x或&mut *x引入特殊的语义。试考虑&mut *x,它可以理解为先获得了lvalue*x,然后对这个lvalue进行了borrow。之所以称其为reborrow,是因为该lvalue所在的place实际上已经被xborrow过了,我们又「重新borrow」了一次,reborrow的特殊之处就在于Rust允许我们borrow一个已经被borrow过的lvalue。
更进一步,形如&(*ref).x的borrow也是允许的,也可以视为一种reborrow,即我们可以单独reborrow一个已经被borrow过的lvalue的一部分。
实际上reborrow的行为也是有其适用范围的:
- 被shared borrow的lvalue,不能被reborrow为mutable reference,只能被reborrow为shared reference
- 被unique immutable borrow的lvalue,不能被reborrow为mutable reference,但能被reborrow为shared reference
- 被mutable borrow的lvalue,可以被reborrow为shared reference或mutable reference
需要注意的是,当我们使用&mut *xreborrow了x后,我们就将无法使用x,直到该reborrow expire为止,例如下列代码就是错误的:
1234 | let a = &mut 3;let b = &mut *a; // a is reborrowed*a = 4; // a is shadowed, ERROR*b = 5; // reborrow expired |
|---|
应该说,引入reborrow后,每个memory location都会有一个borrow stack。将borrow stack定义在memory location上而不是place上,是因为memory location没有进一步的结构,可以简化讨论,而place可以有子place,其不同的子place可以被分别reborrow,于是构不成一个stack。
borrow stack的规则很简单:
- 对最初的变量的borrow,位于栈底
- 此后的每次reborrow,都会将该reborrow放入栈顶
- reborrow得到的mutable reference,会将栈中更早的mutable reference屏蔽,使其不能被访问或reborrow
- reborrow得到的shared reference,会使栈中更早的mutable reference退化为shared reference,即只能被读取或被reborrow为shared reference
- 栈顶的reborrow一旦expire,其引起的屏蔽效果即会解除
这里忽略了具有interior mutability的shared reference以及raw pointer,真正用于定义Rust语义的borrow stack模型要复杂得多,因为需要考虑unsafe特性与mutable/shared reference的语义的交互。
注意有时实际上可能发生reborrow产生的新reference尚且存活,旧的reference却已经被销毁的情况:
- 例如
let new; { let old = &mut x; new = &*old; }; use(new);,即新reference的作用域比旧reference更大 - 又如
let mut parent = &mut p; let child = &parent.x; let parent = &mut q;,即旧reference被覆盖,从而不可达
此时我们并不会立即将旧的reference从borrow stack中移除,而是会等待栈顶的reborrow先expire,再将旧的borrow移除。