【问题标题】:Mutable borrow issue while looping with reference pivot使用参考枢轴循环时出现可变借用问题
【发布时间】:2021-12-04 01:48:26
【问题描述】:
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 case 中仍然认为 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 导致了这个问题,因为它在没有它的情况下编译通过,但是为什么呢?

【问题讨论】:

    标签: rust borrow-checker


    【解决方案1】:

    这是我对正在发生的事情的看法。为了解释它,我在不改变想法的情况下稍微简化了您的代码:

    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()(我的意思是@987654330 的内部值@ 选项结构)。但是p.grow() 需要使用其中一个引用。

    如果p == &amp;mut a,我们调用p.grow(),那么对a.next.as_mut() 的引用就会失效,从而出错。如果p == a.next.as_mut(),我们调用p.grow(),我们引用&amp;mut a两次,因为第一个借用仍然不能被删除。

    如果不分配p = n,则不存在这样的问题,因为只有一个有效引用,编译器可以在调用p.grow()时使用。

    在这个例子中实际上有 3 种方法可以防止编译器错误:

    1. 删除p = n 分配。
    2. p.grow() 移动到 if let 块内,从而确保在调用时 p 引用内部值。
    3. 退出 main 以防 next 是 Some:
    if let Some(n) = next {
      p = n;
    } else {
      return;
    }
    
    p.grow();
    

    后一种方法是可行的,因为编译器确定在p.grow() 调用处首先借用不需要并且可以删除,而p 只能指向内部值。

    原始问题中的问题是相同的。编译器必须确定 p 在调用 p.grow() 时引用的内容。

    Lagerbaer提供的解决方案中,p在调用p.grow()时只能引用一个特定的值(此时没有引用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;
                }
            }
        }
    

    【讨论】:

    • 谢谢。这是对借用检查器实际上存在问题的一个很好的解释!
    • @Maxim 对于p == a.next.as_mut() 的情况,您能否详细说明为什么&amp;mut a 被引用两次?我想这就是我想知道的:只有一个 p(在我的示例中没有 next 变量),当您为 p 分配一个新值时,不应该已经“释放”了 p 中的旧借用吗?毕竟,无论如何,没有办法有效地同时访问具有两个可变借用的 a 。这就是为什么我对第二次借用感到如此困惑......
    • 问题不在于借用本身(如修复错误的 3 种方法所示),而是编译器不知道实际使用了哪一种借用,因此它必须保持两者有效。但是从双重可变借用的角度来看,在其中一个上调用 p.grow() 会使另一个无效。编译器无法删除另一个借用,即使代码中没有任何内容指向它,因为它不知道要删除哪个借用。
    • @Maxim 现在很清楚了。谢谢!
    【解决方案2】:

    为什么要在匹配表达式中借用?那是不必要的。只需匹配p.next。这本身并没有移动。移动的是您匹配的模式类型。那么我们匹配Some(ref mut n),而不是匹配Some(n)

    https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=1859dc15cdc08ddec8cac3efb6d8fa31

    【讨论】:

    • 我只是好奇为什么这不起作用...为什么这是借用检查器的预期行为?
    • 当你写 match &amp;mut p.next 时,你在可变地借用 p.next。但是借用结构的字段总是涉及借用整个结构。所以现在p 是可变借用的。借用是整个匹配块的生命。在此期间,访问和/或改变p 的唯一合法方法是通过借用,但您可以尝试通过其中一种方法直接改变p。这就是借用检查器抱怨的原因。
    • 嗯......我也认为它是你提到的。但有趣的是,删除p = n 就可以通过,我们如何解释这种现象?
    • 另外,将p.grow()(第二个可变借用)移出对原始代码没有帮助...如果在匹配范围内可变借用 p 是问题所在,那么为什么外部声明在我的第二个例子中仍然不起作用?我认为这是最让我困惑的部分......
    猜你喜欢
    • 2021-11-05
    • 2018-10-11
    • 2020-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-03-05
    • 1970-01-01
    • 1970-01-01
    • 2015-12-14
    相关资源
    最近更新 更多