如何在安全的Rust中表达相互递归的数据结构?

xjreopfe  于 2022-12-19  发布在  其他
关注(0)|答案(4)|浏览(128)

我试图在Rust中实现一个类似场景图的数据结构。我想要一个等价于这个用 safe Rust表示的C++代码:

struct Node
{
    Node* parent; // should be mutable, and nullable (no parent)
    std::vector<Node*> child;

    virtual ~Node() 
    { 
        for(auto it = child.begin(); it != child.end(); ++it)
        {
            delete *it;
        }
    }

    void addNode(Node* newNode)
    {
        if (newNode->parent)
        {
            newNode->parent.child.erase(newNode->parent.child.find(newNode));
        }
        newNode->parent = this;
        child.push_back(newNode);
    }
}

我想要的属性:

  • 父节点获得其子节点的所有权
  • 节点可通过某种引用从外部访问
  • 触及一个Node的事件可能会改变整个树
vwoqyblh

vwoqyblh1#

Rust试图通过禁止你做可能不安全的事情来确保内存的安全性,因为这种分析是在编译时执行的,所以编译器只能推理出已知安全的操作子集。
在Rust中,您可以轻松地存储对父对象的引用(通过借用父节点,从而防止变异)* 或 * 子节点列表(拥有它们,这给了你更多的自由),但 * 不是两者都有 *(不使用unsafe)。这对于addNode的实现尤其成问题,因为它需要对给定节点的父节点进行可变访问。如果你把parent指针存储为一个可变引用,那么,由于一个特定对象一次只能有一个可变引用可用,访问父对象的唯一方法就是通过子节点,你只能有一个子节点,否则你会有两个可变引用指向同一个父节点。
如果您想避免不安全的代码,有许多替代方法,但它们都需要一些牺牲。
最简单的解决方案是简单地删除parent字段,我们可以定义一个单独的数据结构,以便在遍历树时记住节点的父节点,而不是将其存储在节点本身中。
首先,让我们定义Node

#[derive(Debug)]
struct Node<T> {
    data: T,
    children: Vec<Node<T>>,
}

impl<T> Node<T> {
    fn new(data: T) -> Node<T> {
        Node { data: data, children: vec![] }
    }

    fn add_child(&mut self, child: Node<T>) {
        self.children.push(child);
    }
}

(我添加了一个data字段,因为节点上没有数据的树不是非常有用!)
现在让我们定义另一个struct来在导航时跟踪父对象:

#[derive(Debug)]
struct NavigableNode<'a, T: 'a> {
    node: &'a Node<T>,
    parent: Option<&'a NavigableNode<'a, T>>,
}

impl<'a, T> NavigableNode<'a, T> {
    fn child(&self, index: usize) -> NavigableNode<T> {
        NavigableNode {
            node: &self.node.children[index],
            parent: Some(self)
        }
    }
}

impl<T> Node<T> {
    fn navigate<'a>(&'a self) -> NavigableNode<T> {
        NavigableNode { node: self, parent: None }
    }
}

如果在导航树时不需要改变树,并且可以保留父NavigableNode对象,那么这个解决方案就可以很好地工作(这对于递归算法来说工作得很好,但是如果要将NavigableNode存储在其他数据结构中并保留它,那么就不太好用了)。如果你想要最大的通用性,你可以使用Borrow trait来允许直接值、借用指针、Box es、Rc 's等等。
现在,让我们讨论一下zippers。(列表、树、Map等),以便访问该元素花费恒定的时间,同时仍然保留该数据结构的所有数据。如果您需要导航树并在导航期间改变它,同时保留向上导航树的能力,然后你可以把一棵树变成一个拉链,并通过拉链执行修改。
下面是我们如何为上面定义的Node实现zippers:

#[derive(Debug)]
struct NodeZipper<T> {
    node: Node<T>,
    parent: Option<Box<NodeZipper<T>>>,
    index_in_parent: usize,
}

impl<T> NodeZipper<T> {
    fn child(mut self, index: usize) -> NodeZipper<T> {
        // Remove the specified child from the node's children.
        // A NodeZipper shouldn't let its users inspect its parent,
        // since we mutate the parents
        // to move the focused nodes out of their list of children.
        // We use swap_remove() for efficiency.
        let child = self.node.children.swap_remove(index);

        // Return a new NodeZipper focused on the specified child.
        NodeZipper {
            node: child,
            parent: Some(Box::new(self)),
            index_in_parent: index,
        }
    }

    fn parent(self) -> NodeZipper<T> {
        // Destructure this NodeZipper
        let NodeZipper { node, parent, index_in_parent } = self;

        // Destructure the parent NodeZipper
        let NodeZipper {
            node: mut parent_node,
            parent: parent_parent,
            index_in_parent: parent_index_in_parent,
        } = *parent.unwrap();

        // Insert the node of this NodeZipper back in its parent.
        // Since we used swap_remove() to remove the child,
        // we need to do the opposite of that.
        parent_node.children.push(node);
        let len = parent_node.children.len();
        parent_node.children.swap(index_in_parent, len - 1);

        // Return a new NodeZipper focused on the parent.
        NodeZipper {
            node: parent_node,
            parent: parent_parent,
            index_in_parent: parent_index_in_parent,
        }
    }

    fn finish(mut self) -> Node<T> {
        while let Some(_) = self.parent {
            self = self.parent();
        }

        self.node
    }
}

impl<T> Node<T> {
    fn zipper(self) -> NodeZipper<T> {
        NodeZipper { node: self, parent: None, index_in_parent: 0 }
    }
}

要使用这个zippers,你需要拥有树的根节点的所有权。通过获得节点的所有权,zippers可以移动东西,以避免复制或克隆节点。当我们移动zippers时,我们实际上是丢弃旧的zippers并创建一个新的zippers(虽然我们也可以通过修改self来实现,但我认为这样更清楚,而且它允许您链接方法调用)。
如果上述选项不令人满意,并且必须绝对地将节点的父节点存储在节点中,则次佳选项是使用Rc<RefCell<Node<T>>>来引用父节点,使用Weak<RefCell<Node<T>>>来引用子节点。Rc启用共享所有权,但在运行时执行引用计数会增加开销。RefCell启用内部可变性,但是增加了开销以在运行时跟踪活动的借用。Weak类似于Rc,但是它不递增引用计数;这用于中断引用循环,这将防止引用计数下降到零,从而导致存储器泄漏。对于使用X1 M19 N1 X、X1 M20 N1 X和X1 M21 N1 X的实现,X1 E5 F1 X。

krcsximq

krcsximq2#

问题是这种数据结构本质上是不安全的;它在Rust中没有一个不使用unsafe的直接等价物,这是设计好的。
如果你想把它翻译成安全的Rust代码,你需要更具体地说明你到底想从它那里得到什么。我知道你在上面列出了一些属性,但是经常有人来Rust会说“我想要我在这个C/C代码中拥有的一切”,而直接的答案是“嗯,你 * 不能 *”。
你也不可避免地要改变你处理这个问题的方式,你给出的例子中的指针没有任何所有权语义、可变别名和循环;所有这些Rust都不允许你像C
那样简单地忽略掉。
最简单的解决方案是去掉parent指针,并在外部维护它(像文件系统路径一样),这也很适合借用,因为任何地方都没有循环:

pub struct Node1 {
    children: Vec<Node1>,
}

如果你 * 需要 * 父指针,你可以中途使用Ids:

use std::collections::BTreeMap;

type Id = usize;

pub struct Tree {
    descendants: BTreeMap<Id, Node2>,
    root: Option<Id>,
}

pub struct Node2 {
    parent: Id,
    children: Vec<Id>,
}

BTreeMap实际上是您的“地址空间”,通过 * 不 * 直接使用内存地址来绕过借用和别名问题。
当然,这会引入一个给定的Id没有绑定到特定树的问题,这意味着它所属的节点可能会被破坏,现在你得到的是一个“有效的”悬空指针,但是,这是你为别名和变异付出的代价,而且也不那么直接。
或者,您可以全力以赴,使用引用计数和动态借用检查:

use std::cell::RefCell;
use std::rc::{Rc, Weak};

// Note: do not derive Clone to make this move-only.
pub struct Node3(Rc<RefCell<Node3_>>);

pub type WeakNode3 = Weak<RefCell<Node3>>;

pub struct Node3_ {
    parent: Option<WeakNode3>,
    children: Vec<Node3>,
}

impl Node3 {
    pub fn add(&self, node: Node3) {
        // No need to remove from old parent; move semantics mean that must have
        // already been done.
        (node.0).borrow_mut().parent = Some(Rc::downgrade(&self.0));
        self.children.push(node);
    }
}

在这里,可以使用Node3在树的不同部分之间转移节点的所有权,使用WeakNode3进行外部引用,或者,可以使Node3可克隆,并在add中添加回逻辑,以确保给定节点不会意外地成为错误父节点的子节点。
严格来说,这并不比第二种方法好,因为 * 这种 * 设计绝对 * 不能 * 受益于静态借用检查。第二种方法 * 至少 * 可以防止您在编译时同时从两个地方改变图;给你,如果那样的话,你会崩溃的。
重点是:你不可能拥有所有的东西,你必须决定你真正需要支持的操作。在这一点上,通常只是选择类型来给予你必要的属性。

cnjp1d6j

cnjp1d6j3#

在某些情况下,你也可以使用一个 * 竞技场 。竞技场保证存储在其中的值将与竞技场本身具有相同的生存期。这意味着添加更多的值不会使任何现有的生存期无效,但移动竞技场会。因此,如果你需要 * 返回 * 树,这样的解决方案是不可行的。
这通过从节点本身移除所有权来解决问题。
这里有一个例子,它也使用了 * internal mutability
来允许节点在创建后发生变化。在其他情况下,如果树只被构造了一次,然后简单地导航,你可以删除这种变化。

use std::{
    cell::{Cell, RefCell},
    fmt,
};
use typed_arena::Arena; // 1.6.1

struct Tree<'a, T: 'a> {
    nodes: Arena<Node<'a, T>>,
}

impl<'a, T> Tree<'a, T> {
    fn new() -> Tree<'a, T> {
        Self {
            nodes: Arena::new(),
        }
    }

    fn new_node(&'a self, data: T) -> &'a mut Node<'a, T> {
        self.nodes.alloc(Node {
            data,
            tree: self,
            parent: Cell::new(None),
            children: RefCell::new(Vec::new()),
        })
    }
}

struct Node<'a, T: 'a> {
    data: T,
    tree: &'a Tree<'a, T>,
    parent: Cell<Option<&'a Node<'a, T>>>,
    children: RefCell<Vec<&'a Node<'a, T>>>,
}

impl<'a, T> Node<'a, T> {
    fn add_node(&'a self, data: T) -> &'a Node<'a, T> {
        let child = self.tree.new_node(data);
        child.parent.set(Some(self));
        self.children.borrow_mut().push(child);
        child
    }
}

impl<'a, T> fmt::Debug for Node<'a, T>
where
    T: fmt::Debug,
{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:?}", self.data)?;
        write!(f, " (")?;
        for c in self.children.borrow().iter() {
            write!(f, "{:?}, ", c)?;
        }
        write!(f, ")")
    }
}

fn main() {
    let tree = Tree::new();
    let head = tree.new_node(1);
    let _left = head.add_node(2);
    let _right = head.add_node(3);

    println!("{:?}", head); // 1 (2 (), 3 (), )
}
xbp102n0

xbp102n04#

TL;DR:DK.的第二个版本无法编译,因为parent有self.0以外的其他类型,通过将其转换为WeakNode来修复。另外,在下面一行中,“self”没有“children”属性,但self.0有。
我更正了DK的版本。所以它编译和工作。下面是我的代码:
dk_树.rs

use std::cell::RefCell;
use std::rc::{Rc, Weak};

// Note: do not derive Clone to make this move-only.
pub struct Node(Rc<RefCell<Node_>>);

pub struct WeakNode(Weak<RefCell<Node_>>);

struct Node_ {
    parent: Option<WeakNode>,
    children: Vec<Node>,
}

impl Node {
    pub fn new() -> Self {
        Node(Rc::new(RefCell::new(Node_ {
            parent: None,
            children: Vec::new(),
        })))
    }
    pub fn add(&self, node: Node) {
        // No need to remove from old parent; move semantics mean that must have
        // already been done.
        node.0.borrow_mut().parent = Some(WeakNode(Rc::downgrade(&self.0)));
        self.0.borrow_mut().children.push(node);
    }
    // just to have something visually printed
    pub fn to_str(&self) -> String {
        let mut result_string = "[".to_string();
        for child in self.0.borrow().children.iter() {
            result_string.push_str(&format!("{},", child.to_str()));
        }
        result_string += "]";
        result_string
    }
}

然后是www.example.com中的main函数main.rs:

mod dk_tree;

use crate::dk_tree::{Node};

fn main() {
    let root = Node::new();
    root.add(Node::new());
    root.add(Node::new());
    let inner_child = Node::new();
    inner_child.add(Node::new());
    inner_child.add(Node::new());
    root.add(inner_child);
    let result = root.to_str();
    println!("{result:?}");
}

我让WeakNode更像Node的原因是为了在两者之间进行更容易的转换

相关问题