web-dev-qa-db-ja.com

安全なRustで相互再帰的なデータ構造を表現するにはどうすればよいですか?

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

私が欲しいプロパティ:

  • 親が子の所有権を取得します
  • ノードは、ある種の参照を介して外部からアクセスできます
  • 1つのNodeに触れるイベントは、ツリー全体を変更する可能性があります
25
nulleight

Rustは、安全でない可能性のあることを行うことを禁止することにより、メモリの安全性を確保しようとします。この分析はコンパイル時に実行されるため、コンパイラーは、安全であることがわかっている操作のサブセットについてのみ推論できます。

Rustでは、簡単に保存できますどちらか親への参照(親を借用して突然変異を防ぐことにより)または子ノードのリスト(それらを所有することにより、より自由に)、しかし両方ではないunsafeを使用せずに)。これは、特定のノードの親への可変アクセスを必要とするaddNodeの実装にとって特に問題です。ただし、parentポインタを可変参照として格納する場合、特定のオブジェクトへの可変参照は一度に1つしか使用できないため、親にアクセスする唯一の方法は子ノードを使用することです。 、および子ノードは1つしか持てません。そうでない場合は、同じ親ノードへの2つの可変参照があります。

安全でないコードを避けたい場合は、多くの選択肢がありますが、それらはすべていくつかの犠牲を必要とします。


最も簡単な解決策は、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フィールドを追加しました!)

次に、ナビゲートするときに親を追跡する別の構造体を定義しましょう。

#[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を他のデータ構造に格納し、それを維持したい)。 2番目の制限は、借用したポインター以外のものを使用して親を格納することで緩和できます。最大の汎用性が必要な場合は、 Borrow trait を使用して、直接値、借用ポインター、Boxes、Rcなどを許可できます。


それでは、 zippers について話しましょう。関数型プログラミングでは、ジッパーを使用してデータ構造の特定の要素(リスト、ツリー、マップなど)に「焦点を合わせる」ため、そのデータ構造のすべてのデータを保持しながら、その要素へのアクセスに一定の時間がかかります。ツリーをナビゲートし、ナビゲーション中にツリーをmutateする必要がある場合は、ツリーを上にナビゲートする機能を維持しながら、ツリーをジッパーとジッパーを介して変更を実行します。

上記で定義したNodeのジッパーを実装する方法は次のとおりです。

#[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 }
    }
}

このジッパーを使用するには、ツリーのルートノードの所有権が必要です。ノードの所有権を取得することにより、ジッパーはノードのコピーやクローン作成を回避するために物事を移動できます。ジッパーを移動するときは、実際に古いジッパーを削除して新しいジッパーを作成します(ただし、selfを変更することでも可能ですが、その方が明確だと思いました。さらに、メソッド呼び出しを連鎖させることができます)。 。


上記のオプションでは不十分で、ノードの親をノードに絶対に格納する必要がある場合、次善のオプションはRc<RefCell<Node<T>>>を使用して親と子を参照することです。 Rc 共有所有権を有効にしますが、実行時に参照カウントを実行するためのオーバーヘッドを追加します。 RefCell は内部の可変性を有効にしますが、実行時にアクティブな借用を追跡するためにオーバーヘッドを追加します。 DKの回答を参照RcおよびRefCellを使用した実装の場合。

28
Francis Gagné

問題は、このデータ構造が本質的に安全ではないことです。 haveunsafeを使用しないRustの直接の同等物。これは仕様によるものです。

これを安全なRustコードに変換したい場合は、正確に何を求めているのかをより具体的にする必要があります。上記のいくつかのプロパティをリストしたことは知っていますが、多くの場合、= Rustは「このC/C++コードにあるすべてのものが欲しい」と言うでしょう。これに対する直接の答えは「まあ、あなたできません」です。

あなたはまた、必然的に、これへのアプローチ方法を変更する必要があります。あなたが与えた例には、所有権のセマンティクス、変更可能なエイリアシング、およびサイクルのないポインターがあります。これらすべてRustでは、C++のように単純に無視することはできません。

最も簡単な解決策は、parentポインターを削除し、それを外部で維持することです(ファイルシステムパスなど)。どこにもサイクルがないので、これは借用にもうまく機能します。

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

need親ポインタの場合、途中でIDを使用できます。

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は事実上「アドレス空間」であり、メモリアドレスを直接使用することで借用やエイリアシングの問題を回避しますnot

もちろん、これにより、特定のIdが特定のツリーに関連付けられていないという問題が発生します。つまり、それが属するノードが破壊される可能性があり、これで事実上 aダングリングポインタ。しかし、それはエイリアシングとミューテーションを持つためにあなたが支払う代償です。また、それほど直接的ではありません。

または、完全に独り占めして、参照カウントと動的借用チェックを使用することもできます。

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にロジックを追加して、特定のノードが誤って間違った親の子のままにならないようにすることもできます。

this設計は絶対にcannot静的な借用チェックの恩恵を受けるため、これは2番目のオプションよりも厳密には優れていません。 2番目のものは少なくともコンパイル時に一度に2つの場所からグラフを変更することを防ぐことができます。ここで、それが起こった場合、あなたはただクラッシュするでしょう。

重要なのは、すべてだけを持つことはできないということです。 実際にサポートする必要のある操作を決定する必要があります。その時点で、それは通常、必要なプロパティを提供するタイプを選択する場合にすぎません。

20
DK.

場合によっては、arenaを使用することもできます。アリーナは、そこに格納されている値がアリーナ自体と同じ存続期間を持つことを保証します。つまり、値を追加しても既存の有効期間は無効になりませんが、アリーナを移動すると無効になります。したがって、ツリーをreturnする必要がある場合、このようなソリューションは実行できません。

これは、ノード自体から所有権を削除することで問題を解決します。

これもinterior 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 (), )
}
12
Shepmaster