Обеспечение родительско-дочерних отношений в C# и.Net

Давайте возьмем следующие два класса:

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

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

Свойство Parent дочернего объекта всегда должно возвращать CollectionOfChildren, в котором находится Child, или значение NULL, если дочерний элемент отсутствует в такой коллекции. Между этими двумя классами этот инвариант должен поддерживаться и не должен быть взломан (ну, легко) потребителем класса.

Как вы реализуете такие отношения? CollectionOfChildren не может установить ни одного из закрытых членов Child, так как же он должен сообщить Child, что он был добавлен в коллекцию? (Бросок исключения допустим, если ребенок уже является частью коллекции.)


internal ключевое слово было упомянуто. Я сейчас пишу приложение для WinForms, так что все в одной сборке, и это, по сути, ничем не отличается от public,

4 ответа

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

Мой ответ содержит решения - первый использует вложенные классы, чтобы позволить внутреннему классу получить доступ к внешнему классу. Позже я понял, что нет необходимости обращаться к закрытым данным другого класса, следовательно, нет необходимости во вложенных классах, если методы получения и установки свойств разработаны тщательно, чтобы избежать бесконечных косвенных рекурсий.

Чтобы избежать проблемы с 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<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 свойство, которое инициирует обновление дочерней коллекции родителя и предотвращает вход в цикл 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<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:

/// <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:

public interface IChildItem<P> where P : class
{
    P Parent { get; 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);

Окончательная реализация также имеет идентификаторы для детей и позволяет удалять детей. Но последнее разрушает строгое соблюдение отношений родитель-ребенок.

Может ли эта последовательность работать на вас?

  • вызов 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 в качестве дочернего способа проверки "подлинности" родителя.

Имейте в виду, что вы можете все это отразить отражением, так что вам действительно нужно лишь немного усложнить передвижение, чтобы удержать людей. Использование Томасом явных интерфейсов является еще одним способом сдерживания, хотя я думаю, что это немного сложнее.

Другие вопросы по тегам