2011-01-23 15 views
15

Antes incluso pido, me deja la respuesta obvia fuera del camino: La interfaz ICollection<T> incluye un método Remove para eliminar un elemento arbitrario, que Queue<T>Stack<T> y realmente no puede soportar (ya que sólo se pueden eliminar "fin" elementos).¿Por qué Queue (T) y Stack (T) no implementan ICollection (T)?

Bien, me doy cuenta de eso. En realidad, mi pregunta no es específicamente sobre los tipos de colección Queue<T> o Stack<T>; más bien, se trata de la decisión de diseño de no implementar ICollection<T> para cualquier tipo genérico que sea esencialmente una colección de valores T.

Esto es lo que me parece extraño. Digamos que tengo un método que acepta una colección arbitraria de T, y para el propósito del código que estoy escribiendo, sería útil conocer el tamaño de la colección. Por ejemplo (el código de abajo es trivial, ya que sólo la ilustración!):

// Argument validation omitted for brevity. 
static IEnumerable<T> FirstHalf<T>(this ICollection<T> source) 
{ 
    int i = 0; 
    foreach (T item in source) 
    { 
     yield return item; 
     if ((++i) >= (source.Count/2)) 
     { 
      break; 
     } 
    } 
} 

Ahora, no hay realmente ninguna razón para que este código no podría operar en un Queue<T> o una Stack<T>, excepto que esos tipos no implementan ICollection<T>. Ellos hacen implemento ICollection, por supuesto-estoy adivinando principalmente para la propiedad Count solos, pero que conduce a la optimización de código extraño como esto:

// OK, so to accommodate those bastard Queue<T> and Stack<T> types, 
// we will just accept any IEnumerable<T>... 
static IEnumerable<T> FirstHalf<T>(this IEnumerable<T> source) 
{ 
    int count = CountQuickly<T>(source); 
    /* ... */ 
} 

// Then, assuming we've got a collection type with a Count property, 
// we'll use that... 
static int CountQuickly<T>(IEnumerable collection) 
{ 
    // Note: I realize this is basically what Enumerable.Count already does 
    // (minus the exception); I am just including it for clarity. 
    var genericColl = collection as ICollection<T>; 
    if (genericColl != null) 
    { 
     return genericColl.Count; 
    } 

    var nonGenericColl = collection as ICollection; 
    if (nonGenericColl != null) 
    { 
     return nonGenericColl.Count; 
    } 

    // ...or else we'll just throw an exception, since this collection 
    // can't be counted quickly. 
    throw new ArgumentException("Cannot count this collection quickly!"); 
} 

¿No tendría más sentido que acaba de abandonar el ICollection interfaz completamente (no me refiero a abandonar la implementación, por supuesto, ya que eso sería un cambio radical, solo quiero decir, dejar de usarlo), y simplemente implementar ICollection<T> con implementación explícita para los miembros que no tienen un perfecto ¿partido?

Quiero decir, mira lo que ICollection<T> ofertas:

  • Count - Queue<T>Stack<T> y ambos tienen esto.
  • IsReadOnly - Queue<T> y Stack<T> fácilmente podría tener esto.
  • Add - Queue<T> podría implementar esto explícitamente (con Enqueue), como podría Stack<T> (con Push).
  • Clear - Comprobar.
  • Contains - Comprobar.
  • CopyTo - Comprobar.
  • GetEnumerator - Comprobar (duh).
  • Remove - Este es el único que Queue<T> y Stack<T> no tienen una combinación perfecta.

Y aquí es el verdadero problema: ICollection<T>.Removedevuelve un bool; por lo que una implementación explícita de Queue<T> podía totalmente (por ejemplo) comprobar si el elemento que desea eliminar es en realidad el elemento de cabeza (usando Peek), y si es así, llame Dequeue y volver true, de lo contrario volver false. Stack<T> fácilmente se podría dar una implementación similar con Peek y Pop.

Muy bien, ahora que he escrito sobre mil palabras sobre por qué que piensan que esto sería posible, planteo la pregunta obvia: ¿por no lo hicieron los diseñadores de Queue<T> y Stack<T> implementar esta interfaz ? Es decir, ¿cuáles fueron los factores de diseño (que probablemente no estoy considerando) que llevaron a la decisión de que esta sería la elección incorrecta? ¿Por qué se implementó ICollection en su lugar?

Me pregunto si, en el diseño de mis propios tipos, hay algunos principios rectores que debería considerar con respecto a la implementación de la interfaz que podría pasar por alto al hacer esta pregunta. Por ejemplo, ¿se considera una mala práctica implementar explícitamente interfaces que no son totalmente compatibles en general (si es así, parece estar en conflicto con, por ejemplo, List<T> implementando IList)? ¿Existe una desconexión conceptual entre el concepto de una cola/pila y qué representa ICollection<T>?

Básicamente, tengo la sensación de que debe haber una buena razón Queue<T> (por ejemplo) no lo hace implementar ICollection<T>, y yo no quiero ir a ciegas diseñar mis propios tipos de interfaces e implementación de una inadecuada manera sin estar informado y pensando completamente en lo que estoy haciendo.

Me disculpo por la súper larga pregunta.

+4

Sólo otro ejemplo de las bibliotecas de recolección pobres de MS. Este problema podría haberse resuelto al menos separando las interfaces en versiones de solo lectura y de lectura y escritura (con la versión de lectura-escritura heredando de la versión de solo lectura) y también teniendo interfaces más finas, centrándose solo en una aspecto específico de una colección en lugar de incluir propiedades y métodos irrelevantes. Por lo tanto, ICollection no debería haber incluido Agregar, Borrar o Eliminar. Eso debería haberse dejado, por ejemplo, IMutableCollection . El ICollection podría ser implementado por más clases. – siride

+0

@siride: su vista de las bibliotecas de colección MS refleja la mía. Es molesto que cosas como las propiedades indexadas tengan que definirse dos veces para las versiones de solo lectura y de lectura y escritura, pero tal es la vida. – supercat

+0

+1 para que la innovación implemente 'Remove' solo, eso fue genial :) Y no, esta super-long está bien escrita q. – nawfal

Respuesta

5

No puedo dar la respuesta "¿cuál fue el pensamiento real?" - tal vez uno de los diseñadores nos dará el real pensando y puedo eliminar esto.

Sin embargo, poner a mí mismo en la mentalidad de "lo que si alguien viene a mí para tomar esta decisión", puedo pensar en una respuesta .. Permítanme ilustrar con este código:

public void SomeMethod<T>(ICollection<T> collection, T valueA, T valueB) 
{ 

    collection.Add(valueA); 
    collection.Add(valueB); 

    if(someComplicatedCondition()) 
    { 
    collection.Remove(valueA); 
    } 
} 

(Claro, cualquiera puede crear una mala implementación de ICollection<T>, pero esperamos que el marco establezca el ejemplo). Supongamos que Stack/Queue implementa como lo indica en la pregunta. Entonces, ¿está el código de arriba a la derecha o tiene errores en el borde del caso porque se debe marcar ICollection<T>.Remove()? Si valueA DEBE eliminarse, ¿cómo puedo arreglar esto para que funcione con Stack y Queue? Hay respuestas, pero obviamente el código anterior es incorrecto en este caso, aunque huele razonable.

Así que ambas interpretaciones son válidas, pero estoy de acuerdo con la decisión tomada aquí - si tuviera el código anterior y supiera que podría aprobar una cola o pila que podría diseñar a su alrededor, pero seguro sería una fosa de insecto fácil caer en (donde sea que vea ICollection<T>, recuerde las cajas de borde para eliminar!)

+0

Creo que este es un excelente ejemplo que hace que el razonamiento aquí sea muy concreto. Gracias por ayudarme a entender esto. En el futuro, voy a desafiarme a mí mismo para encontrar ejemplos en los que la implementación de una determinada interfaz "de forma incompleta" cause un comportamiento altamente inesperado; en tales casos, pensaré mucho antes de comprometerme con esas interfaces. –

+0

+1 para * framework para establecer el ejemplo *. Así poner. – nawfal

1

En última instancia, tal vez no son ajustes ideales; Si desea una lista o colección - utilizar un List<T> o Collection<T>; p

Re Add - hay una suposición implícita de que Add añade al final de la colección, lo cual no es el caso de una pila (aunque es para una cola). De alguna manera, realmente me confunde que el enumerador de la pila/cola no hace dequeue/pop, ya que en gran medida espero que los elementos en una cola/pila se obtengan una vez cada uno (y solo una vez).

Tal vez también (de nuevo, usando mi empadronador como sólo un ejemplo) la gente simplemente no pudieron ponerse de acuerdo sobre exactamente cómo debe comportarse en algunos de esos escenarios, y carece totalmente de acuerdo, simplemente no implementarlas era una mejor elección.

+0

@Marc: No hay problema; las personas mezclan esos dos todo el tiempo (sé que tengo). Sé lo que quieres decir sobre enumerar en colas/pilas; ¿Quién alguna vez enumera una cola sin querer dequeue de ella? Supongo que parte del problema es que la documentación para 'IEnumerator (T)' dice: "Los enumeradores se pueden usar para leer los datos en la colección, pero no se pueden usar para modificar la colección subyacente". Como que se arrinconaron allí. –

+0

@Dan Tao: la solución sería no tener IStack y IQueue implementando IEnumerable, sino que deben proporcionar métodos DequeueAll o PopAll que devolverían un IEnumerable. Tenga en cuenta que un método PopAll para un IStack seguro para subprocesos podría arrojar resultados diferentes de la extracción manual de todo de la pila, ya que un PopAll seguro de subprocesos debería garantizar que un elemento empujado alrededor de la hora de PopAll sería el elemento superior devuelto o debería no ser devuelto, sino permanecer en la pila; una secuencia manual de operaciones 'pop' no tendría tal garantía. – supercat

+0

@supercat: estoy de acuerdo, sin duda hay buenas maneras de llevar a cabo este comportamiento sin utilizar la implementación 'IEnumerable '; Dicho esto, no estoy de acuerdo en que 'Queue ' y 'Stack ' * no deberían * implementar 'IEnumerable '. Simplemente parece un caso raro en el que desea enumerar sin eliminar. Personalmente, utilizo métodos de extensión que realizan básicamente el mismo trabajo que las sugerencias 'DequeueAll' y' PopAll'. –

0

Philip ha dado una buena respuesta (+1). Existe otra promesa conceptual de que Remove se romperá por Stack<T>. El ICollection<T>.Remove se documenta como:

Elimina la primera aparición de un objeto específico de la ICollection.

Stack<T> es LIFO, incluso si Remove se puso en práctica, se tendrá que quitar la última aparición en el caso de objetos duplicados.

Si no tiene sentido para Stack<T>, es mejor evitarlo por su primo igual y opuesto.


Me hubiera gustado más si MS:

  • no documentó Remove de ICollection<T> así. Eliminar un objeto igual en alguna parte hubiera tenido más sentido considerando cuán diferentes eran las implementaciones internas de varias estructuras. Forzar la eliminación del primer elemento parece influenciado por la búsqueda lineal de estructuras simples como matrices.

  • tenía interfaces para estructuras de cola. Puede ser:

    public interface IPeekable<T> : IEnumerable<T> // or IInOut<T> or similar 
    { 
        int Count { get; } 
    
        bool Contains(T item); 
        T Peek(); 
        void Add(T item); 
        bool Remove(); 
        void Clear(); 
    } 
    
Cuestiones relacionadas