各アイテムが他のアイテムのコレクションを持ち、各アイテムがParent
その親アイテムを指すプロパティを持つオブジェクト階層を作成しています。これはかなり標準的なものです。また、コレクションが属するアイテムを指すプロパティを持つItemsCollection
クラスを継承しています。これも、特に興味深いものではありません。Collection<Item>
Owner
クラスにアイテムが追加されるとItemsCollection
、コレクションのプロパティを使用してアイテムの親が自動的に設定されOwner
、アイテムが削除されると親がクリアされるようにします。
問題は、Parent
セッターが のみで使用可能でItemsCollection
あり、他には使用できないことです。こうすることで、アイテムの親が誰であるかがわかるだけでなく、 内の既存の値をチェックしたり、誰かが勝手に別の値に変更したりすることで、アイテムが複数のコレクションに追加されないようにすることができますParent
。
これを実現する方法として私たちが知っているのは次の 2 つの方法です。
セッターをプライベートとしてマークし、コレクション定義をアイテム自体のスコープ内に囲みます。利点: 完全な保護。欠点: ネストされたクラスによる見苦しいコード。
ISetParent
アイテムに、そのことだけを知っているプライベート インターフェイスを使用しますItemsCollection
。利点: コードがはるかにクリーンになり、理解しやすくなります。欠点: 技術的には、インターフェイスを知っている人なら誰でも、Item
セッターにキャストして取得できます。
技術的には、リフレクションを使えば誰でも何にでもアクセスできますが、それでも...これを行う最善の方法を見つけようとしています。
C++ には、あるクラスのプライベート メンバーを別のクラスで使用できるように指定できる機能があったことは知っていますが、Friend
それが完璧なシナリオになると思いますが、C# にはそのようなものがあるとは知りません。
疑似コードでは (たとえば、簡潔にするためにすべてのプロパティ変更通知などは削除されており、これはコードからコピーしたのではなく、ここに入力しているだけです)、次のようになります...
public class Item
{
public string Name{ get; set; }
public Item Parent{ get; private set; }
public ItemsCollection ChildItems;
public Item()
{
this.ChildItems = new ItemsCollection (this);
}
}
public class ItemsCollection : ObservableCollection<Item>
{
public ItemsCollection(Item owner)
{
this.Owner = owner;
}
public Item Owner{ get; private set; }
private CheckParent(Item item)
{
if(item.Parent != null) throw new Exception("Item already belongs to another ItemsCollection");
item.Parent = this.Owner; // <-- This is where we need to access the private Parent setter
}
protected override void InsertItem(int index, Item item)
{
CheckParent(item);
base.InsertItem(index, item);
}
protected override void RemoveItem(int index)
{
this[index].Parent = null;
base.RemoveItem(index);
}
protected override void SetItem(int index, Item item)
{
var existingItem = this[index];
if(item == existingItem) return;
CheckParent(item);
existingItem.Parent = null;
base.SetItem(index, item);
}
protected override void ClearItems()
{
foreach(var item in this) item.Parent = null; <-- ...as is this
base.ClearItems();
}
}
同様のことをする他の方法はありますか?
ベストアンサー1
私は毎日あなたの問題を解決しなければなりませんが、あなたがしようとしている方法では解決しません。
一歩下がって考えてみましょう。あなたが解決しようとしている根本的な問題は何ですか?一貫性「x は y の子である」という関係と「y は x の親である」という関係が常に一貫していることを確認しようとしています。これは理にかなった目標です。
あなたの仮定は、子コレクションと親参照がローカルのフィールドに格納されているため、すべてのアイテムが子と親の両方を直接知っているというものです。これは論理的に、アイテム x がアイテム y の子になったときに、x.Parent と y.Children の両方を一貫して変更する必要があることを意味します。これにより、あなたが遭遇した問題が提示されます。両方の変更が一貫して行われるように「責任」を負うのは誰でしょうか。そして、それをどのように保証するのでしょうか。のみ「担当」コードは親フィールドを変更することができますか?
トリッキー。
あなたの推測を否定したと仮定します。すべての項目がその子と親の両方を認識している必要はありません。
テクニック 1:
たとえば、「ユニバース」と呼ばれる特別なアイテムが 1 つあり、それがそれ自身を除くすべてのアイテムの祖先であるとします。「ユニバース」は、よく知られた場所に格納されているシングルトンである可能性があります。アイテムに親を尋ねると、実装はユニバースを見つけ、次にユニバースのすべての子孫を検索してアイテムを探し、パスを追跡します。アイテムが見つかったら、完了です。そこにたどり着いた「パス」を 1 つさかのぼると、親が見つかります。さらに良いことに、鎖親の数を計算したい場合は、それを計算してください。
テクニック 2:
ユニバースが大きく、各アイテムの検索に時間がかかる場合は、コストが高くなる可能性があります。別の解決策としては、アイテムをその親にマップするハッシュ テーブルと、アイテムをその子のリストにマップする 2 番目のハッシュ テーブルをユニバースに含めることが考えられます。子 x を親 y に追加すると、「add」メソッドは実際にユニバースを呼び出し、「アイテム x の親が y になりました」と伝え、ユニバースがハッシュ テーブルの更新を行います。アイテムには独自の「接続性」情報は含まれません。これはユニバースが強制する責任です。
その欠点は、ユニバースにサイクルが含まれる可能性があることです。つまり、ユニバースに x の親が y であり、y の親が x であると伝えることができます。これを回避したい場合は、サイクル検出器を作成する必要があります。
テクニック3:
「本物の」木と「見せかけの」木の 2 つの木があると言えます。実際の木は不変で永続的である実際のツリーでは、すべてのアイテムは子を認識しますが、親を認識しません。不変実際のツリーでは、実際のツリーのルートのプロキシであるファサードノードを作成します。そのノードに子を要求すると、各子を囲む新しいファサードノードが作成されます。ファサード ノードの親プロパティを、その子に対してクエリされたノードに設定します。
これで、ファサード ツリーを親ツリーとして扱うことができますが、親関係はツリーをトラバースするときにのみ計算されます。
ツリーを編集する場合は、古い実ツリーを可能な限り再利用して、新しい実ツリーを作成します。次に、新しいファサード ルートを作成します。
このアプローチの欠点は、通常、編集のたびにツリーを上から下へトラバースする場合にのみ機能することです。
C# および VB コンパイラでは、後者のアプローチを使用します。これは、まさに私たちが直面している状況だからです。コード編集後に解析ツリーを再構築すると、以前のテキストの既存の不変の解析ツリーの多くを再利用できます。常にツリーを上から下に走査し、必要な場合にのみ親参照を計算します。