2011-01-23 2 views
15

Avant même que je demande, laissez-moi obtenir la réponse évidente à l'écart: L'interface ICollection<T> inclut une méthode Remove pour supprimer un élément arbitraire, qui Queue<T> et Stack<T> ne peut pas vraiment prendre en charge (car ils ne peuvent supprimer "fin" éléments).Pourquoi la file d'attente (T) et la pile (T) n'implémentent pas ICollection (T)?

OK, je m'en rends compte. En fait, ma question ne concerne pas spécifiquement les types de collection Queue<T> ou Stack<T>; plutôt, il s'agit de la décision de conception de ne pas mettre en œuvre ICollection<T> pour tout type générique qui est essentiellement une collection de valeurs T.

Voici ce que je trouve étrange. Dites que j'ai une méthode qui accepte une collection arbitraire de T, et pour le but du code que j'écris, il serait utile de connaître la taille de la collection. Par exemple (le code ci-dessous est trivial, pour illustration!):

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

Maintenant, il n'y a vraiment aucune raison pour que ce code ne pourrait fonctionner sur un Queue<T> ou Stack<T>, sauf que ces types ne mettent pas en œuvre ICollection<T>. Ils font mettre en œuvre ICollection, bien sûr, Je devine principalement sur la propriété Count seul, mais qui conduit à un code d'optimisation bizarre comme ceci:

// 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!"); 
} 

Ne serait-il plus logique de simplement abandonner le ICollection interface complètement (je ne veux pas dire abandonner l'implémentation, bien sûr, car ce serait un changement de rupture, je veux juste dire, cesser de l'utiliser), et simplement mettre en œuvre ICollection<T> avec une implémentation explicite pour les membres qui n'ont pas rencontre?

Je veux dire, regardez ce ICollection<T> offres:

  • Count - Queue<T> et Stack<T> ont tous deux cela.
  • IsReadOnly - Queue<T> et Stack<T> facilement pourrait avoir ceci.
  • Add - Queue<T> pourrait implémenter cela explicitement (avec Enqueue), tout comme Stack<T> (avec Push).
  • Clear - Vérifier.
  • Contains - Vérifier.
  • CopyTo - Vérifier.
  • GetEnumerator - Vérifiez (duh).
  • Remove - Ceci est le seul que Queue<T> et Stack<T> ne correspondent pas parfaitement.

Et voici le vrai kicker: ICollection<T>.Removerenvoie un bool; si une mise en œuvre explicite pour Queue<T> pourrait tout à fait (par exemple) vérifier si l'élément à supprimer est en fait l'élément de tête (en utilisant Peek), et le cas échéant, appeler Dequeue et retourner true, sinon retour false. Stack<T> pourrait facilement être donné une mise en œuvre similaire avec Peek et Pop.

Bon, maintenant que je l'ai écrit au sujet de mille mots sur pourquoi je pense que ce serait possible, je pose la question évidente: pourquoi ne l'ont pas les concepteurs de Queue<T> et Stack<T> implémenter cette interface ? C'est-à-dire, quels étaient les facteurs de conception (que je ne considère probablement pas) qui ont mené à la décision que ce serait le mauvais choix? Pourquoi est-ce que ICollection a été implémenté à la place? Je me demande si, en concevant mes propres types, il y a des principes directeurs que je devrais considérer en ce qui concerne la mise en œuvre d'interface que je pourrais négliger en posant cette question. Par exemple, est-il simplement considéré comme une mauvaise pratique d'implémenter explicitement des interfaces qui ne sont pas entièrement prises en charge en général (si tel est le cas, cela semble entrer en conflit avec, par exemple, List<T> implémentant IList)? Y at-il un conceptuel déconnecter entre le concept d'une file d'attente/pile et ce que ICollection<T> est censé représenter?

Fondamentalement, je sens qu'il doit y avoir une bonne raison Queue<T> (par exemple) ne le fait pas mettre en œuvre ICollection<T>, et je ne veux pas aller aveuglément la conception avant mes propres types et mettre en œuvre des interfaces dans un inappropriée manière sans être informé et en pleine réflexion à travers ce que je fais.

Je m'excuse pour la question super-longue.

+4

un autre exemple des bibliothèques de collecte pauvres de MS. Ce problème aurait pu être résolu en séparant au minimum les interfaces en versions lecture seule et en lecture-écriture (avec la version en lecture-écriture héritée de la version en lecture seule) et en ayant également des interfaces plus fines, en se concentrant uniquement sur un aspect spécifique d'une collection plutôt que d'inclure des propriétés et des méthodes non pertinentes. ICollection n'aurait donc pas dû inclure Add, Clear ou Remove. Cela aurait dû être laissé à, par exemple, IMableCollection . ICollection pourrait être implémenté par plus de classes. – siride

+0

@siride: Votre vue des bibliothèques de la collection MS reflète la mienne. Il est fastidieux que des choses comme les propriétés indexées soient définies deux fois pour les versions en lecture seule et en lecture-écriture, mais c'est la vie. – supercat

+0

+1 pour l'innovation à implémenter 'Remove' seul, c'était cool :) Et non, ce q super-long est bien écrit. – nawfal

Répondre

5

Je ne peux pas donner la réponse "ce qui était la pensée réelle" - peut-être l'un des concepteurs nous donnera le réel thinkng et je peux le supprimer.

Cependant, me mettre dans l'état d'esprit de « si quelqu'un est venu me prendre cette décision », je peux penser à une réponse .. Illustrons avec ce code:

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

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

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

(Bien sûr, n'importe qui peut créer une mauvaise implémentation de ICollection<T>, mais nous nous attendons à ce que le framework prenne l'exemple). Supposons que l'empilage/mise en file d'attente est implémentée comme indiqué dans la question. Est-ce que le code ci-dessus est correct, ou est-ce qu'il y a des bogues de casse car ICollection<T>.Remove() devrait être vérifié? Si valueA DOIT être retiré, comment puis-je résoudre ce problème pour travailler avec Stack et File d'attente? Il y a des réponses, mais évidemment le code ci-dessus est faux dans ce cas - même si ça sent raisonnable. Donc les deux interprétations sont valides, mais je suis bien avec la décision prise ici - si j'avais le code ci-dessus et je savais qu'on pourrait me faire passer une file d'attente ou une pile que je pourrais concevoir autour, mais ce serait certainement un fosse d'insecte facile à tomber (partout où vous voyez ICollection<T>, rappelez-vous les cas de bord pour enlever!)

+0

Je pense que c'est un excellent exemple qui rend le raisonnement ici très concret. Merci de m'avoir aidé à me faire une idée. Pour aller de l'avant, je me mettrai au défi de trouver des exemples où l'implémentation d'une certaine interface "incomplètement" provoquera un comportement hautement inattendu - dans de tels cas, je réfléchirai longuement avant de m'engager sur ces interfaces. –

+0

+1 pour * framework pour définir l'exemple *. Bien placé. – nawfal

1

En fin de compte peut-être, ils ne sont tout simplement pas des ajustements idéaux; si vous voulez une liste ou d'une collection - utiliser un List<T> ou Collection<T>; p

Re Add - il y a une hypothèse implicite que Add ajoute à la fin de la collection, ce qui est faux d'une pile (bien qu'il soit pour une file d'attente). À certains égards, cela m'embrouille vraiment que l'énumérateur pour empiler/mettre en file d'attente ne fasse pas dequeue/pop, puisque je m'attends en grande partie à ce que les éléments d'une file d'attente soient récupérés une fois (et une fois).

Peut-être aussi (encore une fois, en utilisant mon recenseur juste un exemple) les gens ne pouvaient tout simplement pas d'accord sur la façon dont exactement il doit se comporter dans certains de ces scénarios, et sans accord complet, il suffit de ne pas les mettre en œuvre était un meilleur choix.

+0

@Marc: Pas de problème. les gens mélangent ces deux tout le temps (je sais que j'ai). Je sais ce que vous entendez par énumération des files d'attente/piles; qui énumère jamais une file d'attente sans vouloir la retirer? Je suppose qu'une partie du problème est que la documentation pour 'IEnumerator (T)' dit: "Les énumérateurs peuvent être utilisés pour lire les données dans la collection, mais ils ne peuvent pas être utilisés pour modifier la collection sous-jacente." Sorte de se mettre dans un coin là-bas. –

+0

@Dan Tao: La solution serait de ne pas implémenter IEnumerable par IStack et IQueue, mais plutôt de leur fournir des méthodes DequeueAll ou PopAll qui retourneraient un IEnumerable. Notez qu'une méthode PopAll pour un IStack thread-safe pourrait donner des résultats différents de tout sauter manuellement de la pile, puisqu'une PopAll thread-safe devrait garantir qu'un item poussé autour du temps de PopAll serait soit l'élément le plus haut retourné, soit devrait ne pas être retourné mais rester dans la pile; une séquence manuelle d'opérations «pop» n'aurait pas cette garantie. – supercat

+0

@supercact: Je suis d'accord, il y a certainement de bonnes manières d'accomplir ce comportement sans utiliser l'implémentation 'IEnumerable '; Cela dit, je ne serais pas d'accord que «File d'attente » et «Stack » ne devrait pas * implémenter 'IEnumerable '. Cela semble juste un cas rare où vous voulez énumérer sans enlever. Personnellement, j'utilise des méthodes d'extension qui effectuent essentiellement le même travail que vos suggestions 'DequeueAll' et' PopAll'. –

0

Philippe a donné une bonne réponse (+1). Il y a une autre promesse conceptuelle que Remove cassera pour Stack<T>. Le ICollection<T>.Remove est documenté comme:

Enlève le premier occurrence d'un objet spécifique de la ICollection.

Stack<T> est LIFO, même si Remove a été mis en œuvre, il devra enlever la dernière occurrence dans le cas d'objets en double.

Si cela n'a pas de sens pour Stack<T>, il vaut mieux l'éviter pour son cousin égal et opposé.


je l'aurais aimé mieux si MS:

  • n'a pas documenté Remove pour ICollection<T> comme ça. Supprimer quelque part un objet égal aurait eu plus de sens si l'on considère à quel point les implémentations internes des diverses structures sont très différentes. Forcer la suppression du premier élément ressemble à influencé par la recherche linéaire de structures simples comme les tableaux.

  • avait des interfaces pour les structures de file d'attente. Peut-être:

    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(); 
    } 
    
Questions connexes