Liste <T> ou IList <T> [fermé]

495

Quelqu'un peut-il m'expliquer pourquoi je voudrais utiliser IList sur List en C #?

Question connexe: pourquoi est-il jugé mauvais d'exposerList<T>

Cacahuète
la source
1
recherchez "code vers une interface pas une implémentation" sur Internet et vous pouvez lire toute la journée ... et trouver quelques guerres h0ly.
granadaCoder

Réponses:

446

Si vous exposez votre classe via une bibliothèque que d'autres utiliseront, vous voulez généralement l'exposer via des interfaces plutôt que des implémentations concrètes. Cela vous aidera si vous décidez de modifier l'implémentation de votre classe ultérieurement pour utiliser une classe concrète différente. Dans ce cas, les utilisateurs de votre bibliothèque n'auront pas besoin de mettre à jour leur code car l'interface ne change pas.

Si vous l'utilisez uniquement en interne, vous ne vous en souciez peut-être pas beaucoup et l'utilisation List<T>peut être acceptable.

tvanfosson
la source
19
Je ne comprends pas la différence (subtile), y a-t-il un exemple que vous pourriez me montrer?
StingyJack
134
Supposons que vous ayez initialement utilisé List <T> et que vous vouliez changer pour utiliser un CaseInsensitiveList <T> spécialisé, qui implémentent tous les deux IList <T>. Si vous utilisez le type concret, tous les appelants doivent être mis à jour. S'il est exposé comme IList <T>, l'appelant ne doit pas être changé.
tvanfosson
46
Ah. D'ACCORD. Je comprends ça. Choisir le plus petit dénominateur commun pour un contrat. Merci!
StingyJack
16
Ce principe est un exemple d'encapsulation, l'un des trois piliers de la programmation orientée objet. L'idée est que vous cachez les détails de l'implémentation à l'utilisateur et que vous leur fournissiez plutôt une interface stable. Il s'agit de réduire la dépendance à l'égard des détails qui pourraient changer à l'avenir.
Jason
14
Notez également que T []: IList <T>, de sorte que pour des raisons de performances, vous pouvez toujours échanger le retour d'un List <T> de votre procédure vers le retour d'un T [], et si la procédure est déclarée renvoyer IList <T> (ou peut-être ICollection <T>, ou IEnumerable <T>) rien d'autre n'aurait besoin d'être changé.
yfeldblum
322

La réponse la moins populaire est que les programmeurs aiment prétendre que leur logiciel va être réutilisé dans le monde entier, alors qu'en fait la majorité des projets seront maintenus par un petit nombre de personnes et quelles que soient les belles extraits sonores liés à l'interface, vous vous trompez toi même.

Astronautes d'architecture . Les chances que vous écriviez jamais votre propre IList qui ajoute quelque chose à celles déjà dans le framework .NET sont si éloignées que ce sont des gelées théoriques réservées aux "meilleures pratiques".

Astronautes logiciels

Évidemment, si on vous demande ce que vous utilisez dans une interview, vous dites IList, souriez, et les deux vous semblent heureux d'être si intelligents. Ou pour une API publique, IList. J'espère que vous comprendrez mon point.

Arec Barrwin
la source
65
Je dois être en désaccord avec votre réponse sardonique. Je ne suis pas totalement en désaccord avec le sentiment que la surarchitecture est un vrai problème. Cependant, je pense que, surtout dans le cas des collections, les interfaces brillent vraiment. Disons que j'ai une fonction qui retourne IEnumerable<string>, à l'intérieur de la fonction, je peux utiliser un List<string>pour un magasin de sauvegarde interne pour générer ma collection, mais je veux seulement que les appelants énumèrent son contenu, pas l'ajouter ou le supprimer. Accepter une interface comme paramètre communique un message similaire "J'ai besoin d'une collection de chaînes, ne vous inquiétez pas, je ne la changerai pas."
joshperry
38
Le développement de logiciels consiste à traduire l'intention. Si vous pensez que les interfaces ne sont utiles que pour la construction d'architectures surdimensionnées et grandioses et n'ont pas leur place dans les petits magasins, alors j'espère que la personne assise en face de vous dans l'interview n'est pas moi.
joshperry
48
Quelqu'un a dû dire ça Arec ... même si vous avez toutes sortes de schémas académiques pour chasser le courrier haineux. +1 pour tous ceux qui détestent quand une petite application est chargée d'interfaces et en cliquant sur "trouver la définition" nous emmène ailleurs que la source du problème ... Puis-je emprunter l'expression "Astronautes d'architecture"? Je peux voir que cela sera utile.
Gats
6
Si cela était possible, je vous donnerai 1000 points pour cette réponse. : D
Samuel
8
Je parlais du résultat plus large de la recherche de motifs. Interfaces pour les interfaces communes bonnes, couches supplémentaires pour chasser un motif, mauvaises.
Gats
186

L'interface est une promesse (ou un contrat).

Comme toujours avec les promesses - plus c'est petit, mieux c'est .

Rinat Abdullin
la source
56
Pas toujours mieux, juste plus facile à garder! :)
Kirk Broadhurst
5
Je ne comprends pas ça. La IListpromesse aussi. Quelle est la plus petite et la meilleure promesse ici? Ce n'est pas logique.
nawfal
3
Pour toute autre classe, je serais d'accord. Mais pas pour List<T>, car AddRangen'est pas défini sur l'interface.
Jesse de Wit
3
Ce n'est pas vraiment utile dans ce cas particulier. La meilleure promesse est celle qui est tenue. IList<T>fait des promesses qu'il ne peut tenir. Par exemple, il existe une Addméthode que peut lancer s'il IList<T>se trouve être en lecture seule. List<T>d'autre part est toujours accessible en écriture et tient donc toujours ses promesses. De plus, depuis .NET 4.6, nous avons maintenant IReadOnlyCollection<T>et IReadOnlyList<T>qui sont presque toujours mieux adaptés que IList<T>. Et ils sont covariants. A éviter IList<T>!
ZunTzu
@ZunTzu Je dirais que quelqu'un faisant passer une collection immuable comme une collection mutable a sciemment rompu le contrat présenté - ce n'est pas un problème avec le contrat lui-même, par exemple IList<T>. Quelqu'un pourrait tout aussi bien implémenter IReadOnlyList<T>et faire en sorte que chaque méthode lève également une exception. C'est un peu le contraire de la frappe de canard - vous l'avez appelé un canard, mais il ne peut pas charlatan comme un canard. Cela fait cependant partie du contrat, même si le compilateur ne peut pas l'appliquer. Je fais 100% conviennent que IReadOnlyList<T>ou IReadOnlyCollection<T>doit être utilisé pour l' accès en lecture seule, cependant.
Shelby Oldfield
93

Certaines personnes disent "toujours utiliser IList<T>au lieu de List<T>".
Ils veulent que vous changiez vos signatures de méthode de void Foo(List<T> input)à void Foo(IList<T> input).

Ces gens ont tort.

C'est plus nuancé que ça. Si vous renvoyez un IList<T>élément de l'interface publique à votre bibliothèque, vous vous laissez des options intéressantes pour peut-être faire une liste personnalisée à l'avenir. Vous n'aurez peut-être jamais besoin de cette option, mais c'est un argument. Je pense que c'est l'argument entier pour retourner l'interface au lieu du type concret. Il convient de le mentionner, mais dans ce cas, il présente un grave défaut.

À titre de contre-argument mineur, vous pouvez trouver que chaque appelant en a besoin de List<T>toute façon, et le code d'appel est jonché de.ToList()

Mais bien plus important encore, si vous acceptez une IList comme paramètre, vous feriez mieux de faire attention, car IList<T>et List<T>ne vous comportez pas de la même manière. Malgré la similitude de nom, et malgré le partage d'une interface, ils n'exposent pas le même contrat .

Supposons que vous ayez cette méthode:

public Foo(List<int> a)
{
    a.Add(someNumber);
}

Un collègue utile "refactorise" la méthode à accepter IList<int>.

Votre code est maintenant cassé, car int[]implémente IList<int>, mais est de taille fixe. Le contrat pour ICollection<T>(la base de IList<T>) requiert le code qui l'utilise pour vérifier l' IsReadOnlyindicateur avant d'essayer d'ajouter ou de supprimer des éléments de la collection. Le contrat List<T>ne l'est pas.

Le principe de substitution de Liskov (simplifié) stipule qu'un type dérivé doit pouvoir être utilisé à la place d'un type de base, sans conditions préalables ni postconditions supplémentaires.

On dirait que cela rompt le principe de substitution de Liskov.

 int[] array = new[] {1, 2, 3};
 IList<int> ilist = array;

 ilist.Add(4); // throws System.NotSupportedException
 ilist.Insert(0, 0); // throws System.NotSupportedException
 ilist.Remove(3); // throws System.NotSupportedException
 ilist.RemoveAt(0); // throws System.NotSupportedException

Mais ce n'est pas le cas. La réponse à cela est que l'exemple utilisé IList <T> / ICollection <T> est incorrect. Si vous utilisez un ICollection <T>, vous devez vérifier l'indicateur IsReadOnly.

if (!ilist.IsReadOnly)
{
   ilist.Add(4);
   ilist.Insert(0, 0); 
   ilist.Remove(3);
   ilist.RemoveAt(0);
}
else
{
   // what were you planning to do if you were given a read only list anyway?
}

Si quelqu'un vous passe un tableau ou une liste, votre code fonctionnera bien si vous vérifiez le drapeau à chaque fois et avez un repli ... Mais vraiment; qui fait ça? Ne savez-vous pas à l'avance si votre méthode a besoin d'une liste pouvant accueillir des membres supplémentaires; ne spécifiez-vous pas cela dans la signature de la méthode? Que feriez-vous exactement si vous passiez une liste en lecture seule comme int[]?

Vous pouvez remplacer un List<T>code into qui utilise IList<T>/ ICollection<T> correctement . Vous ne pouvez pas garantir que vous pouvez remplacer un code IList<T>/ ICollection<T>into qui utilise List<T>.

Il y a un appel au principe de responsabilité unique / principe de ségrégation d'interface dans de nombreux arguments pour utiliser des abstractions au lieu de types concrets - dépendent de l'interface la plus étroite possible. Dans la plupart des cas, si vous utilisez un List<T>et que vous pensez que vous pourriez utiliser une interface plus étroite à la place - pourquoi pas IEnumerable<T>? C'est souvent un meilleur ajustement si vous n'avez pas besoin d'ajouter des éléments. Si vous devez ajouter à la collection, utilisez le type de béton, List<T>.

Pour moi IList<T>(et ICollection<T>) c'est la pire partie du framework .NET. IsReadOnlyviole le principe de la moindre surprise. Une classe, telle que Array, qui ne permet jamais d'ajouter, d'insérer ou de supprimer des éléments ne doit pas implémenter une interface avec les méthodes Add, Insert et Remove. (voir également /software/306105/implementing-an-interface-when-you-dont-need-one-of-the-properties )

Est-ce IList<T>un bon choix pour votre organisation? Si un collègue vous demande de modifier une signature de méthode à utiliser à la IList<T>place de List<T>, demandez-lui comment il ajouterait un élément à un IList<T>. S'ils ne savent pas IsReadOnly(et la plupart des gens ne le savent pas), alors ne l'utilisez pas IList<T>. Déjà.


Notez que l'indicateur IsReadOnly provient de ICollection <T> et indique si des éléments peuvent être ajoutés ou supprimés de la collection; mais juste pour vraiment confondre les choses, cela n'indique pas si elles peuvent être remplacées, ce qui peut être le cas dans le cas des tableaux (qui retournent IsReadOnlys == true).

Pour plus d'informations sur IsReadOnly, voir la définition msdn de ICollection <T> .IsReadOnly

perfectionniste
la source
5
Grande, grande réponse. Je voudrais ajouter que, même si IsReadOnlyc'est truepour un IList, vous pouvez toujours modifier ses membres actuels! Peut-être que cette propriété devrait être nommée IsFixedSize?
xofz
(qui a été testé en passant un Arraycomme un IList, c'est pourquoi j'ai pu modifier les membres actuels)
xofz
1
@SamPearson, il y avait une propriété IsFixedSize sur IList dans .NET 1, non incluse dans IList <T> générique de .NET 2.0 où elle a été consolidée avec IsReadOnly, conduisant à un comportement surprenant autour des tableaux. Il y a un blog détaillé sur les différences subtiles ici: enterprisecraftsmanship.com/2014/11/22/…
perfectionniste
7
Excellente réponse! De plus, depuis .NET 4.6, nous avons maintenant IReadOnlyCollection<T>et IReadOnlyList<T>qui sont presque toujours mieux adaptés que IList<T>mais sans la sémantique paresseuse dangereuse de IEnumerable<T>. Et ils sont covariants. A éviter IList<T>!
ZunTzu
37

List<T>est une implémentation spécifique de IList<T>, qui est un conteneur qui peut être adressé de la même manière qu'un tableau linéaire en T[]utilisant un index entier. Lorsque vous spécifiez IList<T>comme type d'argument de la méthode, vous spécifiez uniquement que vous avez besoin de certaines capacités du conteneur.

Par exemple, la spécification d'interface n'applique pas une structure de données spécifique à utiliser. L'implémentation de List<T>arrive aux mêmes performances pour accéder, supprimer et ajouter des éléments en tant que tableau linéaire. Cependant, vous pourriez imaginer une implémentation qui est soutenue par une liste chaînée à la place, pour laquelle ajouter des éléments à la fin est moins cher (temps constant) mais l'accès aléatoire beaucoup plus cher. (Notez que le .NET LinkedList<T>ne pas mettre en œuvre IList<T>.)

Cet exemple vous indique également qu'il peut y avoir des situations où vous devez spécifier l'implémentation, et non l'interface, dans la liste des arguments: Dans cet exemple, chaque fois que vous avez besoin d'une caractéristique de performance d'accès particulière. Ceci est généralement garanti pour une implémentation spécifique d'un conteneur ( List<T>documentation: "Il implémente l' IList<T>interface générique à l'aide d'un tableau dont la taille est dynamiquement augmentée selon les besoins.").

En outre, vous souhaiterez peut-être envisager d'exposer le moins de fonctionnalités dont vous avez besoin. Par exemple. si vous n'avez pas besoin de modifier le contenu de la liste, vous devriez probablement envisager d'utiliser IEnumerable<T>, qui IList<T>s'étend.

ILoveFortran
la source
Concernant votre dernière déclaration sur l'utilisation de IEnumerable au lieu d'IList. Ce n'est pas toujours recommandé, prenez WPF par exemple qui créera simplement un objet IList wrapper et aura donc un impact de perf - msdn.microsoft.com/en-us/library/bb613546.aspx
HAdes
1
Excellente réponse, les garanties de performances sont quelque chose qu'une interface ne peut pas offrir. Dans certains codes, cela peut être très important et l'utilisation de classes concrètes communique votre intention, votre besoin pour cette classe spécifique. Une interface, d'autre part, dit "J'ai juste besoin d'appeler cet ensemble de méthodes, aucun autre contrat impliqué."
joshperry
1
@joshperry Si les performances d'une interface vous dérangent, vous êtes en premier lieu sur la mauvaise plateforme. Impo, c'est de la microoptimisation.
nawfal
@nawfal Je ne commentais pas les avantages / inconvénients des performances des interfaces. J'étais d'accord et commentais le fait que lorsque vous choisissez d'exposer une classe concrète comme argument, vous êtes en mesure de communiquer une intention de performance. Prendre LinkedList<T>vs prendre List<T>vs IList<T>tous communiquent quelque chose des garanties de performance requises par le code appelé.
joshperry
28

Je tournerais la question un peu, au lieu de justifier pourquoi vous devriez utiliser l'interface sur l'implémentation concrète, essayez de justifier pourquoi vous utiliseriez l'implémentation concrète plutôt que l'interface. Si vous ne pouvez pas le justifier, utilisez l'interface.

Patrik Hägne
la source
1
Une façon très intéressante d'y penser. Et une bonne façon de penser lors de la programmation - merci.
Peanut
Bien dit! L'interface étant l'approche la plus flexible, vous devez vraiment justifier ce que l'implémentation concrète vous achète.
Cory House du
9
Excellente façon de penser. Je peux quand même y répondre: ma raison s'appelle AddRange ()
PPC
4
ma raison s'appelle Add (). Parfois, vous voulez une liste dont vous savez qu'elle peut contenir des éléments supplémentaires, sûrement? Voir ma réponse.
perfectionniste
19

IList <T> est une interface qui vous permet d'hériter d'une autre classe et d'implémenter IList <T> tout en héritant de List <T> vous en empêche.

Par exemple, s'il existe une classe A et que votre classe B en hérite, vous ne pouvez pas utiliser List <T>

class A : B, IList<T> { ... }
Diadistis
la source
15

Un principe de TDD et OOP est généralement la programmation vers une interface et non une implémentation.

Dans ce cas spécifique, puisque vous parlez essentiellement d'une construction de langage, pas d'une construction personnalisée, cela n'a généralement pas d'importance, mais dites par exemple que vous avez trouvé que List ne supportait pas quelque chose dont vous aviez besoin. Si vous aviez utilisé IList dans le reste de l'application, vous pourriez étendre List avec votre propre classe personnalisée et être en mesure de le transmettre sans refactoring.

Le coût pour ce faire est minime, pourquoi ne pas vous épargner les maux de tête plus tard? C'est le principe de l'interface.

annakata
la source
3
Si votre ExtendedList <T> hérite de List <T>, vous pouvez toujours l'intégrer dans un contrat List <T>. Cet argument ne fonctionne que si vous écrivez votre propre implémentation d'IList <T> à partir de sratch (ou au moins sans hériter de List <T>)
PPC
14
public void Foo(IList<Bar> list)
{
     // Do Something with the list here.
}

Dans ce cas, vous pouvez passer dans n'importe quelle classe qui implémente l'interface IList <Bar>. Si vous avez utilisé List <Bar> à la place, seule une instance de List <Bar> a pu être transmise.

La méthode IList <Bar> est plus faiblement couplée que la méthode List <Bar>.

smack0007
la source
13

Supposant qu'aucune de ces questions (ou réponses) List vs IList ne mentionne la différence de signature. (C'est pourquoi j'ai cherché cette question sur SO!)

Voici donc les méthodes contenues dans List qui ne se trouvent pas dans IList, au moins à partir de .NET 4.5 (vers 2015)

  • AddRange
  • AsReadOnly
  • Recherche binaire
  • Capacité
  • ConvertAll
  • Existe
  • Trouver
  • Trouver tout
  • FindIndex
  • FindLast
  • FindLastIndex
  • Pour chaque
  • GetRange
  • InsertRange
  • LastIndexOf
  • Enlever tout
  • RemoveRange
  • Inverser
  • Trier
  • ToArray
  • TrimExcess
  • TrueForAll
JasonS
la source
1
Pas tout à fait vrai. Reverseet ToArraysont des méthodes d'extension pour l'interface IEnumerable <T>, dont IList <T> dérive.
Tarec
C'est parce que ceux-ci n'ont de sens que dans Lists et non dans d'autres implémentations de IList.
HankCa
12

Le cas le plus important pour l'utilisation d'interfaces sur des implémentations réside dans les paramètres de votre API. Si votre API prend un paramètre List, toute personne qui l'utilise doit utiliser List. Si le type de paramètre est IList, l'appelant dispose de beaucoup plus de liberté et peut utiliser des classes dont vous n'avez jamais entendu parler, qui n'existaient peut-être même pas lors de l'écriture de votre code.

Michael Borgwardt
la source
7

Que faire si .NET 5.0 remplace System.Collections.Generic.List<T>à System.Collection.Generics.LinearList<T>. .NET possède toujours le nom List<T>mais ils garantissent qu'il IList<T>s'agit d'un contrat. Donc, à mon humble avis, nous (au moins I) ne sommes pas censés utiliser le nom de quelqu'un (bien qu'il s'agisse de .NET dans ce cas) et d'avoir des problèmes plus tard.

En cas d'utilisation IList<T>, l'appelant est toujours assuré que les choses fonctionnent, et l'implémenteur est libre de changer la collection sous-jacente en n'importe quelle implémentation concrète alternative deIList

Soundararajan
la source
7

Tous les concepts sont essentiellement énoncés dans la plupart des réponses ci-dessus concernant pourquoi utiliser l'interface sur des implémentations concrètes.

IList<T> defines those methods (not including extension methods)

IList<T> Lien MSDN

  1. Ajouter
  2. Clair
  3. Contient
  4. Copier
  5. GetEnumerator
  6. Indice de
  7. Insérer
  8. Retirer
  9. RemoveAt

List<T> implémente ces neuf méthodes (sans compter les méthodes d'extension), en plus de cela, il dispose d'environ 41 méthodes publiques, ce qui pèse dans votre considération sur celle à utiliser dans votre application.

List<T> Lien MSDN

yantaq
la source
4

Vous le feriez parce que la définition d'une IList ou d'une ICollection s'ouvrirait pour d'autres implémentations de vos interfaces.

Vous voudrez peut-être avoir un IOrderRepository qui définit une collection de commandes dans IList ou ICollection. Vous pouvez alors avoir différents types d'implémentations pour fournir une liste de commandes tant qu'elles sont conformes aux "règles" définies par votre IList ou ICollection.

Peter Lindholm
la source
3

IList <> est presque toujours préférable selon les conseils de l'autre affiche, mais notez qu'il y a un bogue dans .NET 3.5 sp 1 lors de l'exécution d'une IList <> à travers plus d'un cycle de sérialisation / désérialisation avec WCF DataContractSerializer.

Il existe maintenant un SP pour corriger ce bogue: KB 971030

StuartLC
la source
2

L'interface garantit que vous obtenez au moins les méthodes que vous attendez ; être conscient de la définition de l'interface ie. toutes les méthodes abstraites qui doivent être implémentées par n'importe quelle classe héritant de l'interface. donc si quelqu'un crée sa propre classe avec plusieurs méthodes en plus de celles qu'il a héritées de l'interface pour certaines fonctionnalités supplémentaires, et celles-ci ne vous sont d'aucune utilité, il vaut mieux utiliser une référence à une sous-classe (dans ce cas, le interface) et lui affecter l'objet de classe concret.

l'avantage supplémentaire est que votre code est à l'abri de toute modification de la classe concrète car vous n'êtes abonné qu'à quelques-unes des méthodes de la classe concrète et ce sont celles qui vont être là tant que la classe concrète hérite de l'interface que vous êtes en utilisant. donc sa sécurité pour vous et sa liberté pour le codeur qui écrit une implémentation concrète pour changer ou ajouter plus de fonctionnalités à sa classe concrète.

vishy
la source
2

Vous pouvez regarder cet argument sous plusieurs angles, y compris celui d'une approche purement OO qui dit de programmer contre une interface et non une implémentation. Avec cette pensée, l'utilisation d'IList suit le même principe que le passage et l'utilisation des interfaces que vous définissez à partir de zéro. Je crois également aux facteurs d'évolutivité et de flexibilité fournis par une interface en général. Si une classe implémentant IList <T> doit être étendue ou modifiée, le code consommateur ne doit pas changer; il sait à quoi adhère le contrat IList Interface. Cependant, l'utilisation d'une implémentation concrète et de List <T> sur une classe qui change, pourrait également entraîner la modification du code appelant. En effet, une classe adhérant à IList <T> garantit un certain comportement qui n'est pas garanti par un type concret utilisant List <T>.

Le fait de pouvoir également modifier l'implémentation par défaut de List <T> sur une classe implémentant IList <T>, par exemple le .Add, .Remove ou toute autre méthode IList, donne au développeur beaucoup de flexibilité et de puissance, sinon prédéfini par liste <T>

atconway
la source
0

En règle générale, une bonne approche consiste à utiliser IList dans votre API accessible au public (le cas échéant, et la sémantique de liste est nécessaire), puis List en interne pour implémenter l'API. Cela vous permet de passer à une implémentation différente d'IList sans casser le code qui utilise votre classe.

La liste des noms de classe peut être modifiée dans le prochain framework .net mais l'interface ne changera jamais car l'interface est un contrat.

Notez que, si votre API ne sera utilisée que dans les boucles foreach, etc., vous voudrez peut-être envisager de simplement exposer IEnumerable à la place.

lazydeveloper
la source