在C#和.Net中实施父子关系

我们来看以下两个类:

public class CollectionOfChildren { public Child this[int index] { get; } public void Add(Child c); } public class Child { public CollectionOfChildren Parent { get; } } 

Child的Parent属性应始终返回Child所在的CollectionOfChildren,如果子级不在此类集合中,则返回null。 在这两个类之间,应该保持这个不变量,并且不应该被类的消费者破坏(好的,容易的)。

你是如何实现这种关系的? CollectionOfChildren无法设置Child的任何私有成员,那么它应该如何通知Child它已被添加到集合中? (如果孩子已经是一个集合的一部分,则可以接受例外。)


已提及internal关键字。 我现在正在编写一个WinForms应用程序,所以一切都在同一个程序集中,这基本上与public没有什么不同。

 public class CollectionOfChildren { public Child this[int index] { get; } public void Add(Child c) { c.Parent = this; innerCollection.Add(c); } } public class Child { public CollectionOfChildren Parent { get; internal set; } } 

我的答案包含解决方案 – 第一个使用嵌套类来允许内部类访问外部类。 后来我意识到不需要访问其他类的私有数据,因此如果设计getter和setter属性以避免无限的间接递归,则不需要嵌套的classe。

为了避免internal字段的问题,您可以将集合类嵌套到项类中,并将字段设为private 。 以下代码并不完全符合您的要求,但显示了如何创建一对多关系并使其保持一致。 一个Item可能有一个父母和许多孩子。 当且仅当项目具有父项时,它才会在父项的子集合中。 我在没有测试的情况下编写了代码adhoc,但我认为没有办法从Item类的外部打破这个。

 public class Item { public Item() { } public Item(Item parent) { // Use Parent property instead of parent field. this.Parent = parent; } public ItemCollection Children { get { return this.children; } } private readonly ItemCollection children = new ItemCollection(this); public Item Parent { get { return this.parent; } set { if (this.parent != null) { this.parent.Children.Remove(this); } if (value != null) { value.Children.Add(this); } } } private Item parent = null; 

ItemCollection类嵌套在Item类中以获取对私有字段parent级的访问权。

  public class ItemCollection { public ItemCollection(Item parent) { this.parent = parent; } private readonly Item parent = null; private readonly List items = new List(); public Item this[Int32 index] { get { return this.items[index]; } } public void Add(Item item) { if (!this.items.Contains(item)) { this.items.Add(item); item.parent = this.parent; } } public void Remove(Item item) { if (this.items.Contains(item)) { this.items.Remove(item); item.parent = null; } } } } 

UPDATE

我现在检查了代码(但只是粗略地),我相信它可以在没有嵌套类的情况下工作,但我还没有绝对确定。 这一切都是关于使用Item.Parent属性而不会导致无限循环,但是那里已经存在的检查和我为效率而添加的检查可以防止这种情况 – 至少我相信它。

 public class Item { // Constructor for an item without a parent. public Item() { } // Constructor for an item with a parent. public Item(Item parent) { // Use Parent property instead of parent field. this.Parent = parent; } public ItemCollection Children { get { return this.children; } } private readonly ItemCollection children = new ItemCollection(this); 

重要的部分是Parent属性,它将触发父级子集合的更新并防止进入infinte循环。

  public Item Parent { get { return this.parent; } set { if (this.parent != value) { // Update the parent field before modifing the child // collections to fail the test this.parent != value // when the child collection accesses this property. // Keep a copy of the old parent for removing this // item from its child collection. Item oldParent = this.parent; this.parent = value; if (oldParent != null) { oldParent.Children.Remove(this); } if (value != null) { value.Children.Add(this); } } } } private Item parent = null; } 

ItemCollection类的重要部分是私有parent字段,它使项集合知道其所有者,以及Add()Remove()方法,它们触发添加或删除项的Parent属性的更新。

 public class ItemCollection { public ItemCollection(Item parent) { this.parent = parent; } private readonly Item parent = null; private readonly List items = new List(); public Item this[Int32 index] { get { return this.items[index]; } } public void Add(Item item) { if (!this.items.Contains(item)) { this.items.Add(item); item.Parent = this.parent; } } public void Remove(Item item) { if (this.items.Contains(item)) { this.items.Remove(item); item.Parent = null; } } } 

我最近实现了一个类似于AgileJon的解决方案,以通用集合的forms和子项目实现的接口:

ChildItemCollection

 ///  /// Collection of child items. This collection automatically set the /// Parent property of the child items when they are added or removed ///  /// Type of the parent object /// Type of the child items public class ChildItemCollection : IList where P : class where T : IChildItem

{ private P _parent; private IList _collection; public ChildItemCollection(P parent) { this._parent = parent; this._collection = new List(); } public ChildItemCollection(P parent, IList collection) { this._parent = parent; this._collection = collection; } #region IList Members public int IndexOf(T item) { return _collection.IndexOf(item); } public void Insert(int index, T item) { if (item != null) item.Parent = _parent; _collection.Insert(index, item); } public void RemoveAt(int index) { T oldItem = _collection[index]; _collection.RemoveAt(index); if (oldItem != null) oldItem.Parent = null; } public T this[int index] { get { return _collection[index]; } set { T oldItem = _collection[index]; if (value != null) value.Parent = _parent; _collection[index] = value; if (oldItem != null) oldItem.Parent = null; } } #endregion #region ICollection Members public void Add(T item) { if (item != null) item.Parent = _parent; _collection.Add(item); } public void Clear() { foreach (T item in _collection) { if (item != null) item.Parent = null; } _collection.Clear(); } public bool Contains(T item) { return _collection.Contains(item); } public void CopyTo(T[] array, int arrayIndex) { _collection.CopyTo(array, arrayIndex); } public int Count { get { return _collection.Count; } } public bool IsReadOnly { get { return _collection.IsReadOnly; } } public bool Remove(T item) { bool b = _collection.Remove(item); if (item != null) item.Parent = null; return b; } #endregion #region IEnumerable Members public IEnumerator GetEnumerator() { return _collection.GetEnumerator(); } #endregion #region IEnumerable Members System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return (_collection as System.Collections.IEnumerable).GetEnumerator(); } #endregion }

IChildItem

 public interface IChildItem

where P : class { P Parent { get; set; } }

使用接口的唯一缺点是无法在set访问器上放置internal修饰符……但无论如何,在典型的实现中,此成员将“​​隐藏”在显式实现之后:

 public class Employee : IChildItem { [XmlIgnore] public Company Company { get; private set; } #region IChildItem explicit implementation Company IChildItem.Parent { get { return this.Company; } set { this.Company = value; } } #endregion } public class Company { public Company() { this.Employees = new ChildItemCollection(this); } public ChildItemCollection Employees { get; private set; } } 

当您想要在XML中序列化此类对象时,这尤其有用:您无法序列化Parent属性,因为它会导致循环引用,但您希望保持父/子关系。

这个序列可以适合你吗?

  • 调用CollectionOfChild.Add(Child c)
  • 将子项添加到内部集合
  • CollectionOfChild.Add调用Child.UpdateParent(this)
  • Child.UpdateParent(CollectionOfChild newParent)调用newParent.Contains(this)以确保子项位于该集合中,然后相应地更改Child.Parent的后备。 还必须调用CollectionOfChild.Remove(this)将其自身从旧父集合中删除。
  • CollectionOfChild.Remove(Child)将检查Child.Parent ,以确保它不再是Child.Parent ,然后才能从集合中删除子集。

放下一些代码:

 public class CollectionOfChild { public void Add(Child c) { this._Collection.Add(c); try { c.UpdateParent(this); } catch { // Failed to update parent this._Collection.Remove(c); } } public void Remove(Child c) { this._Collection.Remove(c); c.RemoveParent(this); } } public class Child { public void UpdateParent(CollectionOfChild col) { if (col.Contains(this)) { this._Parent = col; } else { throw new Exception("Only collection can invoke this"); } } public void RemoveParent(CollectionOfChild col) { if (this.Parent != col) { throw new Exception("Removing parent that isn't the parent"); } this._Parent = null; } } 

不确定这是否有效但想法应该如此。 它通过使用Contains作为孩子检查父级“真实性”的方式来有效地创建和内部方法。

请记住,你可以用reflection来消除所有这些,所以你真的只需要让它稍微难以绕过来阻止人们。 托马斯使用显式接口是另一种阻止的方法,尽管我认为这有点难度。