親ポインターへの循環参照はいつ受け入れられますか?


24

このStack Overflowの質問は、ポインターを介して親を参照している子に関するものです。

最初は、デザインがひどいアイデアであるというコメントは非常に重要でした。

私はこれがおそらく最善のアイデアではないことを理解しています。一般的な経験則から、「これをしないでください!」

ただし、このようなことを行う必要がある場合、どのような条件が存在するのか疑問に思います。ここでのこの質問と関連する回答/コメントは、グラフでもこのようなことをしないことを示唆しています。


1
あなたがリンクした質問は、主題に関してかなり包括的なようです。
モニカとの軽さレース

4
@LightnessRacesinOrbit「これをしないでください」は、理由を理解する限り、実際には役に立ちません。
エンダーランド

2
私はそこに「これをしてはいけない」以上のものを見ています。賛否両論は複数の専門家によって議論されています。
モニカとの軽さレース

1
あなたは、トラバースを必要とする双方向のリスト、ある種の循環バッファを持っているかもしれません。おそらく、あなたはゲームで接続された2つの道路を表しているでしょう。
-rhughes

2
私の実際的な経験則は、「子は親なしで存在できるのか?」という質問です。(XmlDocumentsとそのノードを考慮する場合、ノードはドキュメントのツリーコンテキストなしでは存在できません。これはナンセンスです)。答えが「いいえ」の場合、双方向リンクは大丈夫です。一緒にしか存在できない2つのオブジェクトがあります。オブジェクト独立して存在できる場合、これら2つのリンクのいずれかを削除します。
ミカライ

回答:


43

ここで重要なのは、2つのオブジェクトに循環参照があるかどうかではなく、それらの参照が互いの所有権を示しているかどうかです。

2つのオブジェクトが互いに「所有」することはできません。これにより、初期化と削除の順序が扱いにくいジレンマになります。1つはオプションの参照であるか、1つのオブジェクトが他のオブジェクトのライフタイムを管理しないことを示す必要があります。

二重にリンクされたリストを考えてみましょう。2つのノードは相互にリンクしますが、どちらも他方を「所有」しません(リストは両方を所有します)。これは、どちらのノードも他のノードにメモリを割り当てないこと、または他のノードが他のノードのIDまたはライフタイム管理を担当しないことを意味します。

ツリーにも同様の関係がありますが、ツリー内のノードは子を割り当て、親は自分の子を割り当てます。子から親へのリンクはトラバーサルに役立ちますが、やはり所有権を定義しません。

ほとんどのオブジェクト指向設計では、オブジェクトのデータメンバーとしての別のオブジェクトへの参照は所有権を意味します。たとえば、クラスCarとEngineがあるとします。どちらもそれ自体ではあまり役に立ちません。これらのオブジェクト互いに依存していると言えます。有用な作業を実行するには、他のオブジェクトの存在が必要です。しかし、他のどれが「所有」していますか?この場合、自動車はすべての自動車部品が存在する「コンテナ」であるため、自動車はエンジンを所有していると言えます。オブジェクト指向と現実世界のデザインの両方で、車は部品の合計であり、これらの部品はすべて車のコンテキスト内で互いに接続されています。エンジンはCarへの参照を持っているか、TorqueConverterへの参照を持っています。

循環参照デザインの悪臭になる可能性がありますが、必ずしもそうとは限りません。慎重に使用し、正しく文書化すると、データ構造の使用が簡単になります。

参照を親と子の間で双方向に移動せずにツリーを横断してみてください。もちろん、脆弱で複雑なスタックベースのアプローチを考え出すこともできますし、ごく単純な参照ベースのアプローチを使用することもできます。


16

このような設計で考慮すべきいくつかの側面があります。

  • 構造依存性
  • 所有関係(つまり、構成と他の種類の関連付け)
  • ナビゲーションのニーズ

クラス間の構造依存性:

コンポーネントクラスの再利用を目的とする場合は、不要な依存関係を避け、そのような閉じた循環構造を避ける必要があります。

それでも、2つのクラスが概念的に強く相互リンクしている場合があります。この場合、依存関係を回避することは現実的なオプションではありません。例:ツリーとそのリーフ、またはより一般的にはコンポジットとそのコンポーネント

オブジェクトの所有権:

1つのオブジェクトが他のオブジェクトを所有していますか?または、別の言い方をすると、1つのオブジェクトが破壊された場合、他のオブジェクトも破壊されますか?

このトピックはSnowmanによって詳細に説明されているため、ここでは説明しません。

オブジェクト間のナビゲーションのニーズ:

最後の問題は、ナビゲーションの必要性です。私のお気に入りの例、ギャングフォーフォーの複合デザインパターンを見てみましょう。

ガンマ&他 「子コンポーネントから親への参照を維持することで、複合構造のトラバースと管理を簡素化できます」もちろん、体系的なトップダウントラバースを想像できますが、非常に大きな複合オブジェクトの場合は、操作が大幅に遅くなり、指数関数的になります。直接参照、さらには円形でも、コンポジットの操作を大幅に容易にすることができます。

例としては、電子システムのグラフィカルモデルがあります。複合構造は、電子ボード、回路、要素を表すことができます。モデルを表示および操作するには、GUIビューでいくつかの幾何学的プロキシが必要です。その後、トップダウン検索を開始するよりも、ユーザーが選択したGUI要素からコンポーネントにナビゲートし、親であり、関連する兄弟/姉妹要素がどれであるかを見つけることは確かにはるかに簡単です。

もちろん、Gamma&alが指摘したように、循環関係の不変量を保証する必要があります。参照するSOの質問が示しているように、これは難しい場合があります。しかし、完全に管理可能で安全な方法です。

結論

ナビゲーションの必要性を過小評価してはなりません。UMLがモデリング表記で明示的にアドレス指定したのは、理由がないわけではありません。そして、はい、循環参照が必要な完全に有効な状況があります。

唯一のポイントは、時々人々はそのような方向にすぐに行く傾向があるということです。そのため、決定を下すかどうかを決める前に、関連する3つの側面すべてを検討する価値があります。


1
この答えは、私見であり、強く支持されています。OPは、「親子関係がすでに存在する場合、循環参照によってこれを実装してもよいのか」と尋ねました。そのため、構造と「所有権」(ここで言及した意味で)はすでに明確になっています。つまり、どちらかの側に参照を追加するための唯一の基準は、「ナビゲーションのニーズ」と「子供の独立した再利用」の質問だけです。
ドックブラウン

@DocBrown-適格になると、この質問に常に賞金をかけることができます。:-)

1
@ GlenH7:これは、この回答にそれ以上の票を与えず、スノーマンの答えだけを与えます(質問のポイントをいくらか逃していると思います)。
ドックブラウン

1
@DocBrown-しかし、担当者、人、担当者!

...そして、public-private-protectedのような可視性の選択肢を追加すると、9つの異なるオプションが得られます:)
mikalai

8

通常、循環参照は循環依存関係を意味するため、非常に悪い考えです。おそらく循環依存が悪い理由はすでにご存知でしょうが、完全を期すために、tl; drバージョンでは、クラスAとBの両方が相互に依存している場合は、AまたはBなしでAまたはBを理解/修正/最適化/などすることは不可能です他のクラスを同時に理解/修正/最適化/など。これはすぐにコードベースにつながり、すべてを変更しないと何も変更できません。

ただし、邪悪な循環依存関係を作成せずに循環参照を使用すること可能です。これは、機能的な意味で参照が厳密にオプションである限り機能します。そのため、クラスから簡単に削除することができ、たとえ動作が遅くなったとしても動作します。このような循環型の非依存性作成参照の主な使用例は、リンクリスト、ツリー、ヒープなどのノードベースのデータ構造をすばやくトラバースできるようにすることです。たとえば、原則として、二重リンクリストで実行できる操作は、単リンクリストでも実行できます。たまたまいくつかの操作(リスト内を逆方向に移動するなど)がありますが、 O二重リンクバージョン。


6

通常これを行うのは良い考えではない理由は、それがDependency Inversion Principleに違反するためです。人々はこの投稿で私が十分にカバーすることができるよりもはるかに詳細にこれについて多くのことを書いたが、カップリングが非常にきついので、維持するのを難しくすることに要約される。どちらかのクラスを変更すると、ほとんどの場合、もう一方のクラスの変更が必要になりますが、依存関係が一方向のみを指す場合、インターフェイスの片側の変更は分離されます。両方のクラスが抽象インターフェースを指す場合、さらに良いです。

1つの主な例外は、異なる抽象化レベルで2つの異なるクラスを持たず、同じクラスの2つのノード(ツリー、二重リンクリストなど)がある場合です。ここでは、構造的関係よりも抽象関係。アルゴリズムの効率化のための循環参照は受け入れられ、このような場合には奨励されます。


5

[...]グラフでさえ、このようなことをしないことを提案します。

ツリーがトップダウン方式で物事にアクセスする必要がある一方で、ツリーとは異なるデータ構造からボトムアップ方式で物事にアクセスする必要がある場合があります。

例として、四分木はベクトルグラフィックソフトウェアに要素を格納する場合があります。ただし、ユーザーの選択は、ベクトル要素の参照/ポインターの個別の選択リストに保存されます。ユーザーがその選択を削除したい場合は、クアッドツリーを更新する必要があります。トップダウンではなく、リーフから開始してボトムアップでツリーを更新する方がはるかに効率的です。それ以外の場合は、各要素について、ルートからリーフまで作業してから、再度バックアップする必要があります。


1
はい、この例は本当に適切です。コンポジットでのナビゲーションに関する議論で私が説明しようとしたことは、まさにそのようなものです。バックポインターは速度を大幅に加速します。興味深いパフォーマンスの逸話と非常に明確な図をありがとう!+1
クリストフ

@Christophe私もあなたの例が好きです!データ構造を上/後ろにたどることができるのは、単にバックポインターではなく「循環所有権」であるため、実際に質問に適切に答えているかどうかはわかりませんでした。しかし、私は主に質問の最後の「グラフ」部分に応答していました。

2

Doom 3には、親オブジェクトへのポインターを持つ子オブジェクトの例があります。特に侵入型リストを使用します。要約すると、侵入型リストはリンクされたリストに似ていますが、各ノードにはリスト自体へのポインターが含まれている点が異なります。

利点:

  • オブジェクトが複数のリストに同時に存在できる場合、リストノードのメモリを割り当てる必要があるのは一度だけです。

  • オブジェクトを破棄する必要がある場合、各リストを直線的に検索することなく、オブジェクトが含まれているすべてのリストから簡単に削除できます。

これは非常に具体的なシナリオだと思いますが、あなたの質問を理解すると、親オブジェクトへのポインターを含む子オブジェクトの許容可能な使用例です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.