Обеспечение родительско-дочерних отношений в 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 в качестве дочернего способа проверки "подлинности" родителя.
Имейте в виду, что вы можете все это отразить отражением, так что вам действительно нужно лишь немного усложнить передвижение, чтобы удержать людей. Использование Томасом явных интерфейсов является еще одним способом сдерживания, хотя я думаю, что это немного сложнее.