Rustでグラフ処理モジュールを作っています。モジュールのコアは、グラフ内のデータを保持する複数のコンテナーを持つという考えをモデル化しています。たとえば、内部構造がHashMap
またはAdjacencyMatrix
などであるグラフがある場合があります。
これらのコンテナは、特性を実装する必要があります。
trait GraphData<V> {
fn has_edge(&self, v: &V, u: &V) -> bool;
fn nodes(&self) -> Iterator<V>; // Here's the problem...
}
トレイト定義でトレイトを返すことはできません。トレイトオブジェクトを使用する必要があることはわかっていますが、使用したくありませんBox
。コンテナに独自のNodeIter
構造体を提供させたいのですが。ただし、関連する型コンストラクター、パート1:基本的な概念と概要で説明されているのと同じ問題に悩まされます。投稿では、現在Rustに存在しない関連する型コンストラクター(ATC)について説明しています。私GraphData
はCollection
説明されているジェネリックに似ています。
ATCを「シミュレート」するために使用できる回避策、またはこの状況で使用できるRustに固有のパターンはありますか?
動的ディスパッチに依存せずBox
、またはdyn
キーワードを使用することに頼りたくありません。
NodeIter
モジュールで作成したグラフコンテナのタイプごとに構造体を定義し、コンテナ自体の実装内に「ノード」を追加することを考えました。ただし、これはコードの再利用が不十分であることがわかりました。
以下のようアンダースKaseorgによって答えはすでに説明:あなたはあなたのクローニングと一緒に暮らすことができる場合は、ここではGATSを必要としないかもしれないVec
頂点を含みます。しかし、それはおそらくあなたが望むものではありません。代わりに、通常、元のデータを参照するイテレータが必要です。
これを実現するには、実際にはGATを使用するのが理想的です。しかし、それらはまだ言語の一部ではないので、あなたの主な質問に取り組みましょう:ジェネリック関連型をシミュレートする方法はありますか?私は実際にこのトピックについて非常に広範なブログ投稿を書きました:「GATなしで一般化されたストリーミングイテレータ問題を解決する」。
記事の要約:
あなたにとって最も簡単な方法は、イテレータをボックス化し、それをトレイトオブジェクトとして返すことです。
fn nodes(&self) -> Box<dyn Iterator<&'_ V> + '_>
あなたが言ったように、あなたはそれを望まないので、それは終わりです。
トレイトにライフタイムパラメーターを追加し、そのライフタイムを関連するタイプと&self
レシーバーで使用できます。
trait GraphData<'s, V: 's> {
type NodesIter: Iterator<Item = &'s V>;
fn nodes(&'s self) -> Self::NodesIter;
}
struct MyGraph<V> {
nodes: Vec<V>,
}
impl<'s, V: 's> GraphData<'s, V> for MyGraph<V> {
type NodesIter = std::slice::Iter<'s, V>;
fn nodes(&'s self) -> Self::NodesIter {
self.nodes.iter()
}
}
これはうまくいきます!しかし、今あなたはあなたの特性に迷惑な生涯パラメータを持っています。あなたの場合、それは(煩わしさは別として)問題ないかもしれませんが、状況によっては実際には重大な問題になる可能性があるため、これがうまくいく場合とうまくいかない場合があります。
ライフタイムからタイプへのタイプレベル関数として機能するヘルパートレイトを使用することで、ライフタイムパラメーターをより深いレベルにプッシュできます。これにより、ライフタイムパラメータがメインの特性に含まれなくなったため、状況が少し煩わしくなくなりますが、以前の回避策と同じ制限があります。
まったく別のパスに移動して、グラフへの参照を含むイテレーターラッパーを作成することもできます。
これは大まかなスケッチですが、基本的な考え方は機能します。実際の内部イテレータにはグラフへの参照が含まれていません(したがって、そのタイプにはself
有効期間は必要ありません)。代わりに、グラフ参照は特定のタイプに格納され、呼び出しWrap
ごとに内部イテレータに渡されnext
ます。
このような:
trait InnerNodesIter { /* ... */ }
struct Wrap<'graph, G: GraphData, I: InnerNodesIter> {
graph: &'graph G,
iter: I,
}
type NodesIterInner: InnerNodesIter;
fn nodes(&self) -> Wrap<'_, Self, Self::NodesIterInner>;
次に、あなたが実装することができますIterator
のためにWrap
。グラフへの参照を渡すことができる内部イテレータへのインターフェイスが必要です。のようなものfn next(&mut self, graph: &Graph) -> Option<...>
。でインターフェースを定義する必要がありますInnerNodesIter
。
もちろん、これは非常に冗長であることに苦しんでいます。また、イテレータの動作によっては、少し遅くなる場合もあります。
短くて悲しい要約は次のとおりです。すべての状況で機能する満足のいく回避策はありません。
この場合の私の意見:私はこの正確な状況が複数回発生したプロジェクトに取り組んでいます。私の場合、Box
非常に簡単で問題なく機能するソリューションを使用しました。唯一の欠点は速度(割り当てと動的ディスパッチ)ですが、割り当てはタイトなループでは発生せず(グラフが多数あり、それぞれのノードが非常に少ない場合を除く)、オプティマイザーはおそらく機能しますほとんどの場合、動的呼び出しを非仮想化します(結局のところ、実際の型情報は1つの関数境界だけ離れています)。
この記事はインターネットから収集されたものであり、転載の際にはソースを示してください。
侵害の場合は、連絡してください[email protected]
コメントを追加