对可变二叉树的递归:`已经借用:BorrowMutError`

问题描述

我从一个 Vec 的排序节点开始,然后使用这种排序将这些节点连接到一个二叉树中,然后返回基本结构

// Test name
#[derive(Clone)]
struct Struct {
    parent: Option<Rc<RefCell<Struct>>>,superscript: Option<Rc<RefCell<Struct>>>,subscript: Option<Rc<RefCell<Struct>>>,height: u32,center: u32,symbols: VecDeque<u8>
}

最终得到由上述 Struct 组成的二叉树。在这一点上,这些 Struct 是唯一拥有的,所以我认为我可以从使用 Rc<RefCell<Struct>> 转换为 RefCell<Struct>(认为 Box<Struct> 由于内部可变性而不起作用? ),但我不确定这如何或是否有助于解决我遇到的问题。

在此之后,我需要以一种新颖的方式遍历 Struct,并在整个递归过程中改变属于各种 symbols 的各种 Struct,通过调用 {{ 1}}。

我当前执行此操作会导致 .pop_front() 的各种实例。

游乐场链接https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=636c93088f5a431d0d430d42283348f3

它的功能(请原谅复杂的逻辑):

thread 'main' panicked at 'already borrowed: BorrowMutError'

我考虑过使用 fn traverse_scripts(row: Rc<RefCell<Struct>>) { if let Some(superscript_row) = &row.borrow().superscript { if let Some(superscript_symbol) = superscript_row.borrow().symbols.front() { if let Some(current_row_symbol) = row.borrow().symbols.front() { if superscript_symbol < current_row_symbol { println!("^{{{}",*superscript_symbol); superscript_row.borrow_mut().symbols.pop_front(); traverse_scripts(Rc::clone(superscript_row)); } } else { println!("^{{{}",*superscript_symbol); superscript_row.borrow_mut().symbols.pop_front(); traverse_scripts(Rc::clone(superscript_row)); } } } if let Some(subscript_row) = &row.borrow().subscript { if let Some(subscript_symbol) = subscript_row.borrow().symbols.front() { if let Some(current_row_symbol) = row.borrow().symbols.front() { if subscript_symbol < current_row_symbol { print!("_{{{}",*subscript_symbol); subscript_row.borrow_mut().symbols.pop_front(); traverse_scripts(Rc::clone(subscript_row)); } } else { print!("_{{{}",*subscript_symbol); subscript_row.borrow_mut().symbols.pop_front(); traverse_scripts(Rc::clone(subscript_row)); } } } if let Some(current_row_symbol) = row.borrow().symbols.front() { if let Some(parent_row) = &row.borrow().parent { if let Some(parent_symbol) = parent_row.borrow().symbols.front() { if current_row_symbol < parent_symbol { print!(" {}",*current_row_symbol); row.borrow_mut().symbols.pop_front(); traverse_scripts(Rc::clone(&row)); } } } else { print!(" {}",*current_row_symbol); row.borrow_mut().symbols.pop_front(); traverse_scripts(Rc::clone(&row)); } } if let Some(parent_row) = &row.borrow().parent { if let Some(parent_symbol) = parent_row.borrow().symbols.front() { print!("}} {}",*parent_symbol); row.borrow_mut().symbols.pop_front(); traverse_scripts(Rc::clone(parent_row)); } else { print!("}}"); traverse_scripts(Rc::clone(parent_row)); } } } 代替遍历,但鉴于它不是多线程的,我认为没有必要?

我想我可能在这里遗漏了一个相对简单的想法,我非常感谢您的帮助。

如果我在问题中遗漏了任何内容,请发表评论,我会尝试添加

解决方法

当您在 borrow 上调用 borrow_mutRefCell 时,会创建一个保护对象(RefRefMut)以授予对内部值的访问权限只要它存在。这个守卫将锁定 RefCell 直到它超出范围并被销毁。让我们看看 traverse_scripts 的一部分:

if let Some(superscript_row) = &row.borrow().superscript { // row is borrowed
    if let Some(superscript_symbol) = superscript_row.borrow().symbols.front() { // superscript_row is borrowed
        if let Some(current_row_symbol) = row.borrow().symbols.front() { // row is borrowed again
            if superscript_symbol < current_row_symbol {
                println!("^{{{}",*superscript_symbol);
                superscript_row.borrow_mut().symbols.pop_front(); // superscript_row is borrowed mutably (ERROR)
                traverse_scripts(Rc::clone(superscript_row)); // recursive call while row and superscript_row are borrowed (ERROR)
            }
        } else {
            println!("^{{{}",*superscript_symbol);
            superscript_row.borrow_mut().symbols.pop_front(); // superscript_row is borrowed mutably (ERROR)
            traverse_scripts(Rc::clone(superscript_row)); // recursive call while row and superscript_row are borrowed (ERROR)
        } // row is no longer borrowed twice
    } // superscript_row is no longer borrowed
} // row is no longer borrowed

例如,在第一行中,row.borrow() 返回一个 Ref<Struct>。此 Ref 不能立即删除,因为它在 if let 主体期间被 superscript_row 借用。所以它一直活着——直到最后的 }

这是递归调用 traverse_scripts 的问题,因为 Struct整个递归调用期间被借用。任何在调用堆栈中以可变方式借用相同 Struct 的尝试都将失败。 (一成不变地借用它仍然有效。)

在第二行中借用了 superscript_row。这有同样的问题,但它也有一个更直接的问题:它在同一函数中稍后被可变地借用,甚至在递归调用之前。那个 borrow_mut 永远不会成功,因为那时 superscript_row 总是已经被借用了。

要解决这两个问题,我们将做两件事:

  1. 将每个 RefRefMut 守卫存储在其自己的变量中并重新使用该守卫,而不是再次对同一变量调用 borrow()borrow_mut()
  2. 在递归之前,drop 每个仍然活着的守卫,以便在递归调用中仍然没有借用任何东西。

以下是该部分重写后的样子:

{ // This scope will constrain the lifetime of row_ref
    let row_ref = row.borrow();
    if let Some(superscript_row) = &row_ref.superscript {
        let mut child = superscript_row.borrow_mut(); // use borrow_mut here because we know we'll need it later
        if let Some(superscript_symbol) = child.symbols.front() {
            if let Some(current_row_symbol) = row_ref.symbols.front() {
                if superscript_symbol < current_row_symbol {
                    println!("^{{{}",*superscript_symbol);
                    child.symbols.pop_front();
                    drop(child); // child is no longer needed,so drop it before recursing
                    // Since superscript_row borrows from row_ref,we must Rc::clone it before
                    // dropping row_ref so that we can still pass it to traverse_scripts.
                    let superscript_row = Rc::clone(superscript_row);
                    drop(row_ref); // row_ref is no longer needed,so drop it before recursing
                    traverse_scripts(superscript_row);
                }
            } else {
                println!("^{{{}",*superscript_symbol);
                child.symbols.pop_front();
                // see comments earlier
                drop(child);
                let superscript_row = Rc::clone(superscript_row);
                drop(row_ref);
                traverse_scripts(superscript_row);
            }
        }
    } // child is dropped here (if it wasn't already). superscript_row is no longer borrowed
} // row_ref is dropped here (if it wasn't already). row is no longer borrowed

Full-program playground

这看起来很复杂,因为它很复杂。在改变数据结构的同时遍历数据结构是错误的常见来源(在大多数语言中,而不仅仅是 Rust)。看起来,至少在 traverse_scripts 中,需要变异的唯一原因是在 pop_front 上调用 symbols,所以如果您可以重新设计数据结构,使得只有 symbolsRefCell 中,您可以仅使用 & 引用进行遍历,这会容易得多。另一种常见的方法是编写返回新数据结构的函数,而不是原地改变它们。