重新初始化后移动变量的语义为什么不再是移动?

7

以下是一个最简单的示例(Playground):

#[derive(Debug)]
struct Node {
    id: usize,
}

fn main() {
    let mut node = Node { id: 0 };
    let mut lastnode = &mut node;
    let mut last = lastnode; // move
    (*last).id = 1;
    println!("{:?}", last);
    //println!("{:?}", lastnode);

    let mut node2 = Node { id: 2 };
    lastnode = &mut node2;
    last = lastnode; // doesn't move to "last"
    println!("{:p}", last); // "last" and "lastnode" point to the same address
    println!("{:p}", lastnode);
}

为什么第一个 last = lastnode 会导致所有权转移,但第二个 last = lastnode 看起来只是遵循借用语义?

嗯,这是一个有趣的机制。看起来编译器足够优雅,只将last的可变借用设置为last = lastnode; println!("{:p}", last);这两行代码,然后last被销毁,lastnode的可变借用继续存在。如果你尝试同时使用这两个可变借用,就会出现双重借用错误。 - Alexey Larionov
如果我们添加一行新的代码 println!("{:?}", last),编译器会报错:println!("{:p}", lastnode); // immutable borrow occurs here,而 println!("{:p}", last);//mutable borrow later used here。但这并不是移动语义。 - ZyS
1个回答

9

这是一个相当复杂的编译器借用语义问题,结合了非词法生命周期。让我们一步一步地走过这个问题。

let mut node = Node { id: 0 };
let mut lastnode = &mut node; 
let mut last = lastnode; // move

这行代码将lastnode移动到了last,可以通过取消后面的注释来证明:

//println!("{:?}", lastnode);

如果我们取消对这个的注释,编译器将会报错:
error[E0382]: borrow of moved value: `lastnode`
  --> src/main.rs:12:22
   |
8  |     let mut lastnode = &mut node;
   |         ------------ move occurs because `lastnode` has type `&mut Node`, which does not implement the `Copy` trait
9  |     let mut last = lastnode; // move
   |                    -------- value moved here
...
12 |     println!("{:p}", lastnode);
   |                      ^^^^^^^^ value borrowed here after move

就算价值不高,也可以通过显式重新借用lastnode来使此代码正常工作,如下所示。

let mut last = &mut *lastnode; // move no more!

使用重新借用(reborrow)的方式,当绑定到last的引用不再被使用时,它会将借用返回给lastnode。稍后只有重新赋值,将其绑定到另一个引用,因此不会产生任何冲突。

最有趣的部分可能是,尽管操作的结构和顺序相同,在后续步骤中似乎一切“正常运行”。

let mut node2 = Node { id: 2 };
lastnode = &mut node2;
last = lastnode; // doesn't move to "last"
println!("{:p}", last); // "last" and "lastnode" point to the same address
println!("{:p}", lastnode);

如果lastnode被移动到last中,那么编译器就不会允许我们调用println!("{:p}", lastnode)last = lastnode是一个隐式重新借用,在最后一行代码之前被丢弃,这要归功于非词法生命周期。
考虑到隐式重新借用是编译器文档不完整的特性,它归结为一种边缘情况,即当前编译器不知道如何隐式重新借用可变引用以满足所给定的代码。换句话说,当前版本的编译器只能使第二部分的代码工作,但不能使第一部分工作。
这背后的原因可能是两个变量类型如何定义的实现细节。虽然lastnode始终被分配一个对节点的可变引用,但last通过多次分配给lastnode而定义。当进行多个赋值时,这促使编译器第二次重新借用它,可能是为了满足它所强制的生命周期。无论如何,这与语言的任何不变量都没有关系,并且在未来版本中可能会改变。
为了完整起见,这里是隐式重新借用的另一个最小示例,其中添加一个冗余赋值使代码编译。
#[derive(Debug)]
struct Node;

let mut node = Node;
let mut node2 = Node;
let lastnode;
let mut last;
last = &mut node; // <-- remove this to fail
lastnode = &mut node2;
last = lastnode;
println!("{:p}", last);
println!("{:p}", lastnode);

Playground

另外还有:


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接