web-dev-qa-db-fra.com

IEnumerable vs IReadonlyCollection vs ReadonlyCollection pour exposer un membre de liste

J'ai passé pas mal d'heures à réfléchir au sujet de l'exposition des membres de la liste. Dans une question similaire à la mienne, John Skeet a donné une excellente réponse. N'hésitez pas à y jeter un œil.

ReadOnlyCollection ou IEnumerable pour exposer les collections de membres?

Je suis généralement assez paranoïaque pour exposer des listes, surtout si vous développez une API.

J'ai toujours utilisé IEnumerable pour exposer des listes, car il est assez sûr et donne beaucoup de flexibilité. Permettez-moi d'utiliser un exemple ici

public class Activity
{
    private readonly IList<WorkItem> workItems = new List<WorkItem>();

    public string Name { get; set; }

    public IEnumerable<WorkItem> WorkItems
    {
        get
        {
            return this.workItems;
        }
    }

    public void AddWorkItem(WorkItem workItem)
    {
        this.workItems.Add(workItem);
    }
}

Quiconque code contre un IEnumerable est assez sûr ici. Si je décide plus tard d'utiliser une liste ordonnée ou quelque chose, aucun de leur code ne se casse et c'est toujours bien. L'inconvénient de ceci est que IEnumerable peut être restitué dans une liste en dehors de cette classe.

Pour cette raison, de nombreux développeurs utilisent ReadOnlyCollection pour exposer un membre. C'est assez sûr car il ne peut jamais être retransmis dans une liste. Pour moi, je préfère IEnumerable car il offre plus de flexibilité, si jamais je voulais implémenter quelque chose de différent d'une liste.

J'ai trouvé une nouvelle idée que j'aime mieux. Utilisation d'IReadOnlyCollection

public class Activity
{
    private readonly IList<WorkItem> workItems = new List<WorkItem>();

    public string Name { get; set; }

    public IReadOnlyCollection<WorkItem> WorkItems
    {
        get
        {
            return new ReadOnlyCollection<WorkItem>(this.workItems);
        }
    }

    public void AddWorkItem(WorkItem workItem)
    {
        this.workItems.Add(workItem);
    }
}

Je pense que cela conserve une partie de la flexibilité de IEnumerable et est très bien encapsulé.

J'ai posté cette question pour obtenir des commentaires sur mon idée. Préférez-vous cette solution à IEnumerable? Pensez-vous qu'il est préférable d'utiliser une valeur de retour concrète de ReadOnlyCollection? C'est tout un débat et je veux essayer de voir quels sont les avantages/inconvénients que nous pouvons tous trouver.

Merci d'avance pour votre contribution.

[~ # ~] modifier [~ # ~]

Tout d'abord, merci à tous d'avoir contribué à la discussion ici. J'ai certainement appris une tonne de chacun et j'aimerais vous remercier sincèrement.

J'ajoute quelques scénarios et informations supplémentaires.

Il existe certains pièges courants avec IReadOnlyCollection et IEnumerable.

Considérez l'exemple ci-dessous:

public IReadOnlyCollection<WorkItem> WorkItems
{
    get
    {
        return this.workItems;
    }
}

L'exemple ci-dessus peut être converti en liste et muté, même si l'interface est en lecture seule. L'interface, malgré son homonyme, ne garantit pas l'immuabilité. C'est à vous de fournir une solution immuable, vous devez donc retourner une nouvelle ReadOnlyCollection. En créant une nouvelle liste (une copie essentiellement), l'état de votre objet est sain et sauf.

Richiban le dit le mieux dans son commentaire: l'interface garantit seulement ce que quelque chose peut faire, pas ce qu'il ne peut pas faire

Voir ci-dessous pour un exemple.

public IEnumerable<WorkItem> WorkItems
{
    get
    {
        return new List<WorkItem>(this.workItems);
    }
}

Ce qui précède peut être coulé et muté, mais votre objet est toujours immuable.

Une autre instruction en dehors de la boîte serait les classes de collection. Considérer ce qui suit:

public class Bar : IEnumerable<string>
{
    private List<string> foo;

    public Bar()
    {
        this.foo = new List<string> { "123", "456" };
    }

    public IEnumerator<string> GetEnumerator()
    {
        return this.foo.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

La classe ci-dessus peut avoir des méthodes pour muter foo comme vous le souhaitez, mais votre objet ne peut jamais être converti en liste d'aucune sorte et muté.

Carsten Führmann fait un point fantastique sur les déclarations de rendement dans IEnumerables.

Merci à tous encore une fois.

53
Marcel-Is-Hier

En parlant de bibliothèques de classes, je pense qu'IReadOnly * est vraiment utile, et je pense que vous le faites correctement :)

Il s'agit de collection immuable ... Avant il n'y avait que des immuables et agrandir des tableaux était une tâche énorme, donc .net a décidé d'inclure dans le cadre quelque chose de différent, une collection mutable, qui implémente les trucs laids pour vous, mais à mon humble avis, ils ne l'ont pas fait t vous donne une direction appropriée pour immuable qui sont extrêmement utiles, en particulier dans un scénario de concurrence élevée où le partage de choses mutables est toujours un PITA.

Si vous cochez d'autres langues d'aujourd'hui, comme objective-c, vous verrez qu'en fait les règles sont complètement inversées! Ils échangent assez souvent une collection immuable entre différentes classes, en d'autres termes, l'interface expose juste immuable, et en interne ils utilisent une collection mutable (oui, ils l'ont bien sûr), au lieu de cela, ils exposent des méthodes appropriées s'ils veulent laisser les étrangers changer la collection ( si la classe est une classe avec état).

Donc, cette petite expérience que j'ai avec d'autres langues me pousse à penser que la liste .net est si puissante, mais la collection immuable était là pour une raison quelconque :)

Dans ce cas, il ne s'agit pas d'aider l'appelant d'une interface, de lui éviter de changer tout le code si vous changez d'implémentation interne, comme c'est le cas avec IList vs List, mais avec IReadOnly * vous vous protégez, votre classe, pour ne pas être utilisé correctement, pour éviter le code de protection inutile, code que parfois vous ne pouviez pas également écrire (dans le passé, dans un morceau de code, je devais retourner un clone de la liste complète pour éviter ce problème) .

16
Michael Denny

Jusqu'à présent, un aspect important semble manquer dans les réponses:

Lorsqu'un IEnumerable<T> Est retourné à l'appelant, il doit considérer la possibilité que l'objet retourné soit un "flux paresseux", par ex. une collection construite avec "yield return". C'est-à-dire que la pénalité de performance pour produire les éléments du IEnumerable<T> Peut devoir être payée par l'appelant, pour chaque utilisation de l'IEnumerable. (L'outil de productivité "Resharper" le signale en fait comme une odeur de code.)

En revanche, un IReadOnlyCollection<T> Signale à l'appelant qu'il n'y aura pas d'évaluation paresseuse. (La propriété Count, par opposition à la méthode d'extension Count de IEnumerable<T> (Héritée par IReadOnlyCollection<T> Donc il a aussi la méthode), signale la non-paresse. Et il en va de même du fait qu'il ne semble pas y avoir d'implémentations paresseuses d'IReadOnlyCollection.)

Ceci est également valable pour les paramètres d'entrée, car la demande d'un IReadOnlyCollection<T> Au lieu de IEnumerable<T> Signale que la méthode doit itérer plusieurs fois sur la collection. Bien sûr, la méthode pourrait créer sa propre liste à partir du IEnumerable<T> Et itérer dessus, mais comme l'appelant peut déjà avoir une collection chargée à portée de main, il serait logique d'en tirer parti dans la mesure du possible. Si l'appelant n'a qu'un IEnumerable<T> À portée de main, il n'a qu'à ajouter .ToArray() ou .ToList() au paramètre.

Ce que ne fait pas IReadOnlyCollection , c'est d'empêcher l'appelant de transtyper vers un autre type de collection. Pour une telle protection, il faudrait utiliser la classe ReadOnlyCollection<T>.

En résumé, la chose seulement que IReadOnlyCollection<T> Fait par rapport à IEnumerable<T> Est d'ajouter une propriété Count et donc de signaler qu'aucune paresse n'est impliquée.

63
Carsten Führmann

Il semble que vous puissiez simplement retourner un interface appropriée:

...
    private readonly List<WorkItem> workItems = new List<WorkItem>();

    // Usually, there's no need the property to be virtual 
    public virtual IReadOnlyList<WorkItem> WorkItems {
      get {
        return workItems;
      }
    }
...

Puisque le champ workItems est en fait List<T> donc l'idée naturelle à mon humble avis est d'exposer l'interface la plus large qui est IReadOnlyList<T> dans le cas

4
Dmitry Bychenko