简体   繁体   English

在 C# 和 .Net 中强制执行父子关系

[英]Enforcing parent-child relationship in C# and .Net

Let's take the following two classes:让我们来看以下两个类:

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

public class Child
{
    public CollectionOfChildren Parent { get; }
}

Child's Parent property should always return the CollectionOfChildren that the Child is in, or null if the child is not in such a collection. Child 的 Parent 属性应始终返回 Child 所在的 CollectionOfChildren,如果 child 不在此类集合中,则返回 null。 Between these two classes, this invariant should be maintained, and should not be breakable (well, easily) by the consumer of the class.在这两个类之间,这个不变量应该被维护,并且不应该被类的使用者破坏(好吧,很容易)。

How do you implement such a relationship?你如何实现这种关系? CollectionOfChildren cannot set any of the private members of Child, so how is it supposed to inform Child that it was added to the collection? CollectionOfChildren 不能设置 Child 的任何私有成员,那么它应该如何通知 Child 它已添加到集合中? (Throwing an exception is acceptable if the child is already part of a collection.) (如果孩子已经是集合的一部分,则抛出异常是可以接受的。)


The internal keyword has been mentioned.已经提到了internal关键字。 I am writing a WinForms app at the moment, so everything is in the same assembly, and this is essentially no different than public .我目前正在编写一个 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; }
}

My answer contains to solutions - the first one uses nested classes to allow the inner class to access the outer class.我的答案包含解决方案 - 第一个使用嵌套类来允许内部类访问外部类。 Later I realized that there is no need to access private data of the other class, hence no need for nested classe, if the property getters and setters are designed carefully to avoid infinite indirect recursions.后来我意识到不需要访问另一个类的私有数据,因此不需要嵌套类,如果属性 getter 和 setter 被仔细设计以避免无限间接递归。

To avoid the problem with internal fields you can just nest the collection class into the item class and make the field private .为了避免internal字段的问题,您可以将集合类嵌套到项目类中并使该字段成为private The following code is not exactly what you requestes, but shows how to create a one-to-many relationship and keep it consistent.以下代码并不完全符合您的要求,但展示了如何创建一对多关系并保持一致。 An Item may have one parent and many children.一个Item可能有一个父项和许多子项。 If and only if an item has a parent, then it will be in the child collection of the parent.当且仅当一个项目有父项时,它才会在父项的子集合中。 I wrote the code adhoc without testing, but I think there is no way to break this from outsight of the Item class.我在没有测试的情况下编写了临时代码,但我认为没有办法从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;

The ItemCollection class is nested inside the Item class to gain access to the private field parent . 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;
            }
        }
    }
}

UPDATE更新

I checked the code now (but only roughly) and I believe it will work without nesting the classes, but I am not yet absolutly sure.我现在检查了代码(但只是粗略地),我相信它可以在不嵌套类的情况下工作,但我还不确定。 It's all about using the Item.Parent property without causing an infinite loop, but the checks that were already in there and the one I added for efficency protect from this situation - at least I believe it.这完全是关于在不导致无限循环的情况下使用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);

The important part is the Parent property that will trigger the update of the parent's child collection and prevent from entering a infinte loop.重要的部分是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;
}

The the important parts of the ItemCollection class are the private parent field that makes the item collection aware of its owner and the Add() and Remove() methods that trigger updates of the Parent property of the added or removed item. 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;
        }
    }
}

I recently implemented a solution similar to AgileJon's, in the form of a generic collection and an interface to be implemented by child items :我最近实现了一个类似于 AgileJon 的解决方案,以通用集合和由子项实现的接口的形式:

ChildItemCollection<P,T> : 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> : IChildItem<T> :

public interface IChildItem<P> where P : class
{
    P Parent { get; set; }
}

The only drawback of using an interface is that it's not possible to put an internal modifier on the set accessor... but anyway, in a typical implementation, this member would be "hidden" behind an explicit implementation :使用接口的唯一缺点是不可能在 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; }
}

This is particularly useful when you want to serialize this kind of object in XML : you can't serialize the Parent property because it would cause cyclic references, but you want to keep the parent/child relation.当您想在 XML 中序列化此类对象时,这特别有用:您无法序列化 Parent 属性,因为它会导致循环引用,但您想保留父/子关系。

I was also looking into it recently, and thought about really enforcing this relationship as error proof as possible.我最近也在研究它,并考虑真正加强这种关系,尽可能地证明错误。 Additionally, I tried to keep it as general and type safe as possible.此外,我尽量保持它的通用性和类型安全性。 It may be just overengineered, but still I would like to share it.它可能只是过度设计,但我仍然想分享它。

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);
        }
    }
}

Here comes the Example:示例如下:

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)
   {
   }
}

And adding a child to the parent would look like:将孩子添加到父母看起来像:

var parent = new Parent();
var child1 = new Child(parent.ChildCollection);

The final implementation also has Ids for the children and allows removing of children.最终的实现也有孩子的 ID,并允许移除孩子。 But the latter destroys the strong enforcement of the parent child relationship.但后者破坏了亲子关系的强大执行力。

Could this sequence work for you?这个序列对你有用吗?

  • call CollectionOfChild.Add(Child c)调用CollectionOfChild.Add(Child c)
  • add the child to the internal collection将孩子添加到内部集合
  • CollectionOfChild.Add invokes Child.UpdateParent(this) CollectionOfChild.Add调用Child.UpdateParent(this)
  • Child.UpdateParent(CollectionOfChild newParent) calls newParent.Contains(this) to make sure the child is in that collection then change the backing of Child.Parent accordingly. Child.UpdateParent(CollectionOfChild newParent)调用newParent.Contains(this)以确保孩子在该集合中,然后相应地更改Child.Parent的支持。 Also have to call CollectionOfChild.Remove(this) to remove itself from the old parent's collection.还必须调用CollectionOfChild.Remove(this)将自己从旧父级的集合中删除。
  • CollectionOfChild.Remove(Child) would check Child.Parent to make sure it's not the child's collection anymore before it would remove the child from the collection. CollectionOfChild.Remove(Child)会检查Child.Parent以确保它不再是孩子的集合,然后才从集合中删除孩子。

Putting some code down:放下一些代码:

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;
    }
}

Not sure if that works but the idea should.不确定这是否有效,但这个想法应该。 It effectively creates and internal method by using Contains as the child's way to check the "authenticity" of the parent.它通过使用 Contains 作为子级检查父级“真实性”的方式有效地创建了内部方法。

Keep in mind you can blow all this away with reflection so you really only need to make it slightly hard to get around to deter people.请记住,您可以通过反射消除所有这些,因此您实际上只需要稍微难以四处走动以阻止人们。 Thomas' use of explicit interfaces is another way to deter, though I think this is a bit harder. Thomas 使用显式接口是另一种威慑方式,尽管我认为这有点难。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM