2009-06-11 19 views
5

Tomemos las dos clases siguientes: propiedad ParentHacer cumplir relación padre-hijo en C# y .Net

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

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

del niño siempre debe devolver el CollectionOfChildren que el niño está en, o null si el niño no está en tal Una colección. Entre estas dos clases, esta invariante debe mantenerse, y no debe ser rompible (bueno, fácil) por el consumidor de la clase.

¿Cómo se implementa una relación de este tipo? CollectionOfChildren no puede configurar ninguno de los miembros privados de Child, entonces, ¿cómo se supone que debe informar a Child que se ha agregado a la colección? (una excepción es aceptable si el niño ya es parte de una colección.)


La palabra clave internal se ha mencionado. Estoy escribiendo una aplicación WinForms en este momento, por lo que todo está en el mismo ensamblado, y esto no es esencialmente diferente de public.

+0

¿No debería nombrarse a la propiedad SiblingsAndSelf en lugar de Parent? –

+0

Incluso con una aplicación de winforms, puede dividir la aplicación en varios conjuntos. – Bob

+0

@ Brückner: El niño realmente tendrá una referencia a una clase principal, que tiene un CollectionOfChildren. Think ListView, ListViewItemCollection y ListViewItem. @Bob: podría, sí, pero este no es el trabajo de las asambleas. Lógicamente, todo esto pertenece en un .exe: no debería usar ensamblados para mantener invariantes de clase. – Thanatos

Respuesta

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

Estoy escribiendo un programa de WinForms, así que todo está en el mismo ensamblado. interno, según mi entendimiento, no es diferente aquí que público. – Thanatos

+0

Cierto, en el caso definitivamente podría dejar el modificador 'interno'.Tomé su parte de "consumidor de la clase" para decir que esto podría ser parte de una API para el uso de desarrolladores de terceros. – AgileJon

+0

Ah, ya veo. Estaba pensando más en otro código usando la clase, otros programadores, etc. Incluso si soy la única persona que usa una clase, si una clase tiene una invariante lógica, no debería (incluso en mi propio código fuera de esa clase) ser capaz de violar esa invariante. – Thanatos

2

Mi respuesta contiene soluciones: la primera usa clases anidadas para permitir que la clase interna acceda a la clase externa. Más tarde me di cuenta de que no hay necesidad de acceder a los datos privados de la otra clase, por lo tanto, no hay necesidad de classe anidado, si los getters y setters de la propiedad están diseñados cuidadosamente para evitar infinitas recursiones indirectas.

Para evitar el problema con los campos internal, puede anidar la clase de colección en la clase de elemento y crear el campo private. El siguiente código no es exactamente lo que solicita, pero muestra cómo crear una relación de uno a varios y mantenerlo constante. Un Item puede tener un padre y muchos hijos. Si y solo si un elemento tiene un padre, entonces estará en la colección secundaria del padre. Escribí el código adhoc sin probarlo, pero creo que no hay forma de romperlo desde la clase 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; 

La clase ItemCollection está anidado dentro de la clase Item para acceder al campo privado 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; 
      } 
     } 
    } 
} 

ACTUALIZACIÓN

he comprobado el código de ahora (pero sólo aproximadamente) y creo que va a funcionar sin que anidan las clases, pero aún no estoy absolutamente seguro. Se trata de utilizar la propiedad Item.Parent sin causar un bucle infinito, pero las comprobaciones que ya estaban allí y la que agregué para la eficacia protegen de esta situación, al menos eso creo.

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

La parte importante es la propiedad Parent que dará lugar a la actualización de la colección de niño del padre y prevenir la entrada de un bucle 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; 
} 

Las partes importantes de la clase ItemCollection son el campo privado parent que hace que la colección de elementos conscientes de su propietario y los Add() y Remove() métodos que desencadenan cambios de la propiedad Parent del elemento añadido o eliminado.

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

Este usa un campo interno que Thanatos dijo que no funcionaría para él porque todo es interno. –

+0

Creo que no necesita la variable principal interna en el elemento. Puede hacerlo privado y luego asignarlo al elemento. Parente en lugar de item.parent en la Colección de Artículos. Solución impresionante por cierto: +1 –

+0

@Jeroen Huinink Pensé que no puedo usar la propiedad Parent porque activará un ciclo infinito - Item.Parent => ItemCollection.Add/Remove => Item.Parent => .. Para evitar esto, decidí acceder directamente al campo de respaldo. Para evitar el modificador interno anidé la clase ItemCollection dentro de la clase Item, esto permite que ItemCollection acceda a miembros privados de la clase Item. –

0

recientemente he implementado una solución similar a AgileJon de, en forma de una colección genérica y una interfaz para ser implementados por elementos secundarios:

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

El único inconveniente del uso de una interfaz es que no es posible poner un modificador internal en el acceso set ... pero de todos modos, en una implementación típica, esta persona sería "oculta" detrás de una implementación explícita:

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

Esto es particularmente útil cuando desea serializar este tipo de objeto en XML: no puede serializar la propiedad Parent porque causaría referencias cíclicas, pero desea mantener la relación padre/hijo.

+0

Mientras realicé ingeniería inversa en XML, me di cuenta de que necesitaba mostrar información sobre el elemento secundario y su elemento primario dentro de cada vista secundaria. Esta solución parece perfecta, pero teniendo en cuenta que debo buscar padres para encontrar niños que coincidan y puedo crear modelos de vista usando el objeto padre y cada objeto hijo como voy, ¿eso significa que puedo evitar que cada hijo haga referencia a su padre por completo en este caso? ¿O debería mi hijo ver los modelos * no * hacer referencia directamente al padre? – BoltClock

+0

@BoltClock, difícil de responder sin contexto ... sería más fácil si publicaras algún código –

0

¿Podría esta secuencia funcionar para usted?

  • llamada CollectionOfChild.Add(Child c)
  • añadir al niño a la recolección interna
  • CollectionOfChild.Add invoca Child.UpdateParent(this)
  • Child.UpdateParent(CollectionOfChild newParent) llamadas newParent.Contains(this) para asegurarse de que el niño está en esa colección a continuación, cambiar el respaldo de Child.Parent en consecuencia. También debe llamar al CollectionOfChild.Remove(this) para eliminarse de la colección del antiguo padre.
  • CollectionOfChild.Remove(Child) marcaría Child.Parent para asegurarse de que ya no es la colección del niño antes de que elimine al niño de la colección.

Poner un poco de código de abajo:

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

No sé si esto funciona, pero la idea debería. Crea eficazmente un método interno al usar Contains como la forma en que el niño comprueba la "autenticidad" del elemento principal.

Tenga en cuenta que puede volar todo esto con la reflexión por lo que realmente solo tiene que ser un poco difícil de evitar para disuadir a la gente. El uso de interfaces explícitas de Thomas es otra forma de disuadir, aunque creo que esto es un poco más difícil.

Cuestiones relacionadas