[英]Enforcing parent-child relationship in C# and .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,如果 child 不在此类集合中,则返回 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 被仔细设计以避免无限间接递归。
为了避免internal
字段的问题,您可以将集合类嵌套到项目类中并使该字段成为private
。 以下代码并不完全符合您的要求,但展示了如何创建一对多关系并保持一致。 一个Item
可能有一个父项和许多子项。 当且仅当一个项目有父项时,它才会在父项的子集合中。 我在没有测试的情况下编写了临时代码,但我认为没有办法从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<Item> items = new List<Item>();
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;
}
}
}
}
更新
我现在检查了代码(但只是粗略地),我相信它可以在不嵌套类的情况下工作,但我还不确定。 这完全是关于在不导致无限循环的情况下使用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
属性,它将触发父子集合的更新并防止进入无限循环。
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
字段,它使项目集合知道其所有者,以及触发添加或删除项目的Parent
属性更新的Add()
和Remove()
方法。
public class ItemCollection
{
public ItemCollection(Item parent)
{
this.parent = parent;
}
private readonly Item parent = null;
private readonly List<Item> items = new List<Item>();
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 的解决方案,以通用集合和由子项实现的接口的形式:
ChildItemCollection<P,T> :
/// <summary>
/// Collection of child items. This collection automatically set the
/// Parent property of the child items when they are added or removed
/// </summary>
/// <typeparam name="P">Type of the parent object</typeparam>
/// <typeparam name="T">Type of the child items</typeparam>
public class ChildItemCollection<P, T> : IList<T>
where P : class
where T : IChildItem<P>
{
private P _parent;
private IList<T> _collection;
public ChildItemCollection(P parent)
{
this._parent = parent;
this._collection = new List<T>();
}
public ChildItemCollection(P parent, IList<T> collection)
{
this._parent = parent;
this._collection = collection;
}
#region IList<T> 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<T> 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<T> Members
public IEnumerator<T> GetEnumerator()
{
return _collection.GetEnumerator();
}
#endregion
#region IEnumerable Members
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return (_collection as System.Collections.IEnumerable).GetEnumerator();
}
#endregion
}
IChildItem<T> :
public interface IChildItem<P> where P : class
{
P Parent { get; set; }
}
使用接口的唯一缺点是不可能在 set 访问器上放置internal
修饰符……但无论如何,在典型实现中,该成员将“隐藏”在显式实现后面:
public class Employee : IChildItem<Company>
{
[XmlIgnore]
public Company Company { get; private set; }
#region IChildItem<Company> explicit implementation
Company IChildItem<Company>.Parent
{
get
{
return this.Company;
}
set
{
this.Company = value;
}
}
#endregion
}
public class Company
{
public Company()
{
this.Employees = new ChildItemCollection<Company, Employee>(this);
}
public ChildItemCollection<Company, Employee> Employees { get; private set; }
}
当您想在 XML 中序列化此类对象时,这特别有用:您无法序列化 Parent 属性,因为它会导致循环引用,但您想保留父/子关系。
我最近也在研究它,并考虑真正加强这种关系,尽可能地证明错误。 此外,我尽量保持它的通用性和类型安全性。 它可能只是过度设计,但我仍然想分享它。
public class ChildCollection<TChild> : IEnumerable<TChild>
where TChild : ChildCollection<TChild>.Child
{
private readonly List<TChild> childCollection = new List<TChild>();
private void Add(TChild child) => this.childCollection.Add(child);
public IEnumerator<TChild> GetEnumerator() => this.childCollection.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public abstract class Child
{
private readonly ChildCollection<TChild> childCollection;
protected Child(ChildCollection<TChild> childCollection)
{
this.childCollection = childCollection;
childCollection.Add((TChild)this);
}
}
}
示例如下:
public class Parent
{
public ChildCollection<Child> ChildCollection { get; }
public Parent()
{
ChildCollection = new ChildCollection<Child>();
}
}
public class Child : ChildCollection<Child>.Child
{
public Child(ChildCollection<Child> childCollection) : base(childCollection)
{
}
}
将孩子添加到父母看起来像:
var parent = new Parent();
var child1 = new Child(parent.ChildCollection);
最终的实现也有孩子的 ID,并允许移除孩子。 但后者破坏了亲子关系的强大执行力。
这个序列对你有用吗?
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
以确保它不再是孩子的集合,然后才从集合中删除孩子。放下一些代码:
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 作为子级检查父级“真实性”的方式有效地创建了内部方法。
请记住,您可以通过反射消除所有这些,因此您实际上只需要稍微难以四处走动以阻止人们。 Thomas 使用显式接口是另一种威慑方式,尽管我认为这有点难。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.