循环引用时的可变借用问题

3
struct A {
    next: Option<Box<A>>,
}

impl A {
    fn grow(&mut self) {
        self.next = Some(Box::new(A { next: None }));
    }
}

fn main() {
    let mut a = A{ next: Some(Box::new(A { next: None }))};
    let mut p = &mut a;
    // attempt to append to the list
    loop {
        match &mut p.next {
            Some(n) => p = n,
            None => {
                p.grow();
                break;
            }
        }
    }
}

上面的代码是一个更复杂的数据结构中简化逻辑,能够重现借用检查器的投诉。
error[E0499]: cannot borrow `*p` as mutable more than once at a time
  --> t.rs:19:17
   |
16 |         match &mut p.next {
   |               ----------- first mutable borrow occurs here
...
19 |                 p.grow();
   |                 ^
   |                 |
   |                 second mutable borrow occurs here
   |                 first borrow later used here

error: aborting due to previous error

为什么在match语句中p仍然被认为是可变借用的?而且,试图将

p.update()

移出循环也没有帮助:
fn main() {
    let mut a = A{ next: Some(Box::new(A { next: None }))};
    let mut p = &mut a;
    // attempt to append to the list
    loop {
        match &mut p.next {
            Some(n) => p = n,
            None => {
                break;
            }
        }
    }
    p.grow();
}

在这种情况下我得到了相同的错误。我知道p = n是问题的原因,因为如果没有它就可以编译通过,但为什么呢?

2个回答

4

以下是我对情况的看法。为了解释它,我稍微简化了你的代码,但没有改变其想法:

fn main() {
    let mut a = A{ next: Some(Box::new(A { next: None }))};
    let mut p = &mut a;
    let next = &mut p.next;
    if let Some(n) = next {
        p = n;
    }

    p.grow();
}

这段代码会产生与你的代码相同的错误。更有趣的是,在main末尾无论如何都不能以任何方式使用p,即使是println!("{:?}", p)也会产生相同的错误。
编译器检查代码执行的所有方式并发现在调用p.grow()时,p可能指向a,也可能指向a.next.as_mut()(我的意思是a.next选项结构的内部值)。但p.grow()需要使用这些引用之一。
如果p == &mut a,并且我们调用p.grow(),那么对a.next.as_mut()的引用就变得无效了,因此出错了。如果p == a.next.as_mut(),并且我们调用p.grow(),则第一个borrow仍然无法删除,因为我们引用了&mut a两次。
如果您不给p分配n,则没有这样的问题,因为只有一个有效的引用,编译器在调用p.grow()时可以使用它。
实际上,在这个例子中有三种方法可以防止编译器错误:
  1. 删除p = n赋值。
  2. p.grow()移到if let块内,确保在调用时p引用内部值。
  3. 如果next是Some,则退出main:
if let Some(n) = next {
  p = n;
} else {
  return;
}

p.grow();

后者之所以起作用,是因为编译器确切知道在调用p.grow()时第一个借用不再需要并且可以被丢弃,而p只能指向内部值。

原问题的问题也是如此。编译器必须确切知道在调用p.grow()p引用了什么。

在由Lagerbaer提供的解决方案中,在调用p.grow()p只能引用某个确定的值(此时没有引用p.next),因此它可以正常工作:

    let mut p = &mut a;
    // attempt to append to the list
    loop {
        match p.next {
            Some(ref mut n) => p = n,
            None => {
                p.grow();
                break;
            }
        }
    }

1
谢谢。这是一个非常好的解释,说明了借用检查器实际上关注的问题! - cadolphs
@Maxim 对于 p == a.next.as_mut() 的情况,您能详细说明为什么要两次引用 &mut a 吗?我猜这就是我想知道的:在我的示例中只有一个 p(没有 next 变量),当您给 p 赋新值时,旧的借用是否已经“释放”了呢?毕竟,无论如何都不能同时有效地访问具有两个可变借用的 a。这就是为什么我对第二个借用感到困惑的原因... - Determinant
问题不在于借用本身(如三种修复错误的方法所示),而在于编译器不知道哪个借用实际上正在使用,因此它必须保持两者有效。但是,在其中一个上调用p.grow()会使另一个无效,从双重可变借用的角度来看。编译器只是不能放弃另一个借用,即使代码中没有指向它的内容,因为它不知道要放弃哪一个。 - Maxim Gritsenko
@Maxim 现在很清楚了。谢谢! - Determinant

2

我只是好奇为什么它不起作用...为什么借用检查器的预期行为是这样的? - Determinant
1
当你编写match &mut p.next时,你正在可变借用p.next。但是借用结构体的字段总是涉及借用整个结构体。因此现在p被可变地借用了。该借用在整个匹配块的生命周期内都有效。在此期间,唯一合法的访问和/或修改p的方式是通过该借用,但是你尝试通过其方法直接修改p。这就是为什么借用检查器会抱怨的原因。 - cadolphs
嗯...我也认为是你所说的那样。但有趣的是,去掉p = n后程序也能通过,我们该如何解释这种现象? - Determinant
此外,将 p.grow()(第二个可变借用)移出并不会在原始代码中有所帮助... 如果在 match 的作用域中可变借用 p 是问题所在,那么为什么我的第二个示例中的外部语句仍然无法正常工作?我认为这是最让我困惑的部分... - Determinant

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