【问题标题】:Recursion over a mutable binary tree: `already borrowed: BorrowMutError`递归可变二叉树:`已经借用:BorrowMutError`
【发布时间】:2021-03-25 07:45:11
【问题描述】:

我从已排序节点的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>
}

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

在此之后,我需要以一种新颖的方式遍历Structs,并在整个递归过程中通过调用.pop_front()改变属于各种Structs 的各种symbols

我当前的实现会导致thread 'main' panicked at 'already borrowed: BorrowMutError' 的各种实例。

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

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

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));
        }
    }
}

我考虑过使用Arc&lt;Mutex&lt;Struct&gt;&gt; 来代替遍历,但鉴于它不是多线程的,我认为没有必要?

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

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

【问题讨论】:

  • 此时,这些 Structs 是唯一拥有的:可以创建具有唯一所有权的树,但不是这样:每个节点可能由其子中的父链接,以及其父中的子链接。如果你想切换到Box,你必须删除父链接。
  • Mutex 也无济于事,顺便说一句-lock 方法的工作方式与RefCell 上的borrow_mut 相同。所以你最终会遇到同样的问题。

标签: memory-management rust borrowing


【解决方案1】:

当您在RefCell 上调用borrowborrow_mut 时,将创建一个保护对象(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&lt;Struct&gt;。这个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 中,需要突变的唯一原因是在symbols 上调用pop_front,所以如果你可以重新设计数据结构使得只有symbolsRefCell 中,你可以仅使用 &amp; 引用进行遍历,这会容易得多。另一种常见的方法是编写返回新数据结构的函数,而不是在原地改变它们。

【讨论】:

  • 谢谢! (在我的逻辑进行了一些重组后,它起作用了)
  • @JonathanWoollett-light 您还可以通过将每个嵌套的if let ... if ... else 折叠成一个if 来简化此代码,因为内部if 和外部else 是相同的:@987654322 @。代码中有一些不对称可能是逻辑错误,如果修复也可能使其更简单。但我怀疑最大的好处是改变存储节点的方式(例如:带有索引的单个 Vec&lt;Struct&gt;,而不是 Rcs 的海洋)。
猜你喜欢
  • 2016-04-19
  • 2014-03-29
  • 1970-01-01
  • 2010-12-07
  • 2020-09-15
  • 1970-01-01
  • 1970-01-01
  • 2020-07-10
  • 2011-05-08
相关资源
最近更新 更多