IEnumerable vs List - Quoi utiliser? Comment travaillent-ils?

678

J'ai des doutes sur le fonctionnement des énumérateurs et LINQ. Considérez ces deux sélections simples:

List<Animal> sel = (from animal in Animals 
                    join race in Species
                    on animal.SpeciesKey equals race.SpeciesKey
                    select animal).Distinct().ToList();

ou

IEnumerable<Animal> sel = (from animal in Animals 
                           join race in Species
                           on animal.SpeciesKey equals race.SpeciesKey
                           select animal).Distinct();

J'ai changé les noms de mes objets d'origine pour que cela ressemble à un exemple plus générique. La requête elle-même n'est pas si importante. Ce que je veux demander, c'est ceci:

foreach (Animal animal in sel) { /*do stuff*/ }
  1. J'ai remarqué que si j'utilise IEnumerable, quand je débogue et inspecte "sel", qui dans ce cas est l'IEnumerable, il a des membres intéressants: "inner", "external", "innerKeySelector" et "externalKeySelector", ces 2 derniers apparaissent être délégués. Le membre "intérieur" ne contient pas d'instances "Animal", mais plutôt des instances "Espèce", ce qui était très étrange pour moi. Le membre "externe" contient des instances "Animal". Je suppose que les deux délégués déterminent ce qui entre et ce qui en sort?

  2. J'ai remarqué que si j'utilise "Distinct", le "intérieur" contient 6 éléments (c'est incorrect car seulement 2 sont distincts), mais le "extérieur" contient les valeurs correctes. Encore une fois, les méthodes déléguées déterminent probablement cela, mais c'est un peu plus que ce que je sais sur IEnumerable.

  3. Plus important encore, laquelle des deux options est la meilleure en termes de performances?

La conversion de liste mal via .ToList()?

Ou peut-être utiliser directement l'énumérateur?

Si vous le pouvez, veuillez également expliquer un peu ou lancer des liens qui expliquent cette utilisation de IEnumerable.

Axonn
la source

Réponses:

741

IEnumerabledécrit le comportement, tandis que List est une implémentation de ce comportement. Lorsque vous utilisez IEnumerable, vous donnez au compilateur une chance de reporter le travail à plus tard, en optimisant éventuellement en cours de route. Si vous utilisez ToList (), vous forcez le compilateur à réifier immédiatement les résultats.

Chaque fois que j'empile des expressions LINQ, j'utilise IEnumerable, car en spécifiant uniquement le comportement, je donne à LINQ une chance de différer l'évaluation et éventuellement d'optimiser le programme. Rappelez-vous comment LINQ ne génère pas le SQL pour interroger la base de données jusqu'à ce que vous l'énumériez? Considère ceci:

public IEnumerable<Animals> AllSpotted()
{
    return from a in Zoo.Animals
           where a.coat.HasSpots == true
           select a;
}

public IEnumerable<Animals> Feline(IEnumerable<Animals> sample)
{
    return from a in sample
           where a.race.Family == "Felidae"
           select a;
}

public IEnumerable<Animals> Canine(IEnumerable<Animals> sample)
{
    return from a in sample
           where a.race.Family == "Canidae"
           select a;
}

Vous avez maintenant une méthode qui sélectionne un échantillon initial ("AllSpotted"), plus quelques filtres. Alors maintenant, vous pouvez le faire:

var Leopards = Feline(AllSpotted());
var Hyenas = Canine(AllSpotted());

Est-il donc plus rapide d'utiliser List over IEnumerable? Uniquement si vous souhaitez empêcher l'exécution d'une requête plusieurs fois. Mais est-ce mieux dans l'ensemble? Eh bien dans ce qui précède, les Léopards et les Hyènes sont convertis en requêtes SQL simples chacun , et la base de données ne renvoie que les lignes pertinentes. Mais si nous avions renvoyé une liste de AllSpotted(), elle peut s'exécuter plus lentement car la base de données pourrait renvoyer beaucoup plus de données que ce qui est réellement nécessaire, et nous perdons des cycles en effectuant le filtrage dans le client.

Dans un programme, il peut être préférable de différer la conversion de votre requête en liste jusqu'à la fin, donc si je vais énumérer plusieurs fois à travers Léopards et Hyènes, je ferais ceci:

List<Animals> Leopards = Feline(AllSpotted()).ToList();
List<Animals> Hyenas = Canine(AllSpotted()).ToList();
Chris Wenham
la source
11
Je pense qu'ils se réfèrent aux deux côtés d'une jointure. Si vous sélectionnez "SELECT * FROM Animals JOIN Species ...", la partie interne de la jointure est Animals et la partie externe est Species.
Chris Wenham
10
Quand j'ai lu les réponses sur: IEnumerable <T> vs IQueryable <T> J'ai vu l'explication analogique, de sorte que IEnumerable force automatiquement le runtime à utiliser LINQ to Objects pour interroger la collection. Je suis donc confus entre ces 3 types. stackoverflow.com/questions/2876616/…
Bronek
4
@Bronek La réponse que vous avez liée est vraie. IEnumerable<T>sera LINQ-To-Objects après la première partie, ce qui signifie que tout repéré devra être retourné pour exécuter Feline. D'un autre côté, un IQuertable<T>permettra à la requête d'être affinée, en tirant uniquement sur les félins repérés.
Nate
21
Cette réponse est très trompeuse! Le commentaire de @ Nate explique pourquoi. Si vous utilisez IEnumerable <T>, le filtre va se produire côté client, quoi qu'il arrive.
Hans
5
Oui, AllSpotted () serait exécuté deux fois. Le plus gros problème avec cette réponse est la déclaration suivante: "Eh bien dans ce qui précède, les léopards et les hyènes sont convertis en requêtes SQL simples chacune, et la base de données ne renvoie que les lignes pertinentes." Ceci est faux, car la clause where est appelée sur un IEnumerable <> et qui ne sait que parcourir les objets qui proviennent déjà de la base de données. Si vous effectuiez le retour d'AllSpotted () et des paramètres de Feline () et Canine () dans IQueryable, le filtre se produirait en SQL et cette réponse aurait du sens.
Hans
178

Il y a un très bon article écrit par: TechBlog de Claudio Bernasconi ici: Quand utiliser IEnumerable, ICollection, IList et List

Voici quelques points de base sur les scénarios et les fonctions:

entrez la description de l'image ici entrez la description de l'image ici

rubStackOverflow
la source
25
Il convient de noter que cet article ne concerne que les parties publiques de votre code, pas les rouages ​​internes. Listest une mise en œuvre IListet en tant que telle a des fonctionnalités supplémentaires au - dessus de ceux IList(par exemple Sort, Find, InsertRange). Si vous vous forcez à utiliser IListplus List, vous perdez ces méthodes dont vous pourriez avoir besoin
Jonathan Twite
4
N'oubliez pasIReadOnlyCollection<T>
Dandré
2
Il peut également être utile d'inclure []ici un tableau simple .
jbyrd
Bien qu'il puisse être mal vu, merci d'avoir partagé ce graphique et cet article
Daniel
134

Une classe qui implémente IEnumerablevous permet d'utiliser la foreachsyntaxe.

Fondamentalement, il a une méthode pour obtenir l'élément suivant dans la collection. Il n'a pas besoin que toute la collection soit en mémoire et ne sait pas combien d'éléments il foreachcontient , continue à obtenir l'élément suivant jusqu'à épuisement.

Cela peut être très utile dans certaines circonstances, par exemple dans une table de base de données massive, vous ne voulez pas copier le tout dans la mémoire avant de commencer à traiter les lignes.

maintenant ListoutilsIEnumerable , mais représente toute la collection en mémoire. Si vous en avez un IEnumerableet que vous appelez, .ToList()vous créez une nouvelle liste avec le contenu de l'énumération en mémoire.

Votre expression linq renvoie une énumération et, par défaut, l'expression s'exécute lorsque vous parcourez à l'aide de foreach. Une IEnumerableinstruction linq s'exécute lorsque vous itérez leforeach , mais vous pouvez la forcer à itérer plus tôt en utilisant .ToList().

Voici ce que je veux dire:

var things = 
    from item in BigDatabaseCall()
    where ....
    select item;

// this will iterate through the entire linq statement:
int count = things.Count();

// this will stop after iterating the first one, but will execute the linq again
bool hasAnyRecs = things.Any();

// this will execute the linq statement *again*
foreach( var thing in things ) ...

// this will copy the results to a list in memory
var list = things.ToList()

// this won't iterate through again, the list knows how many items are in it
int count2 = list.Count();

// this won't execute the linq statement - we have it copied to the list
foreach( var thing in list ) ...
Keith
la source
2
Mais que se passe-t-il si vous exécutez un foreach sur un IEnumerable sans le convertir d'abord en List ? Est-ce que cela ramène toute la collection en mémoire? Ou, instancie-t-il l'élément un par un, car il itère sur la boucle foreach? merci
Pap
@Pap ce dernier: il s'exécute à nouveau, rien n'est automatiquement mis en cache en mémoire.
Keith
semble que la différence clé est 1) le tout en mémoire ou non. 2) IEnumerable permettez-moi d'utiliser foreachtandis que List ira par exemple index. Maintenant, si je voudrais connaître le nombre / la longueur d' thingavance, IEnumerable n'aidera pas, non?
Jeb50
@ Jeb50 Pas exactement - à la fois Listet Arraymettre en œuvre IEnumerable. Vous pouvez considérer IEnumerablecomme le plus petit dénominateur commun qui fonctionne à la fois dans les collections de mémoire et les grandes qui obtiennent un élément à la fois. Lorsque vous appelez, IEnumerable.Count()vous pouvez appeler une .Lengthpropriété rapide ou parcourir toute la collection - le fait est que IEnumerablevous ne savez pas. Cela peut être un problème, mais si vous y allez, foreachvous ne vous en souciez pas - votre code fonctionnera avec un Arrayou DataReaderle même.
Keith
1
@MFouadKajj Je ne sais pas quelle pile vous utilisez, mais il est presque certain de ne pas faire de demande avec chaque ligne. Le serveur exécute la requête et calcule le point de départ de l'ensemble de résultats, mais n'obtient pas le tout. Pour les petits ensembles de résultats, il s'agit probablement d'un seul voyage, pour les grands, vous envoyez une demande pour plus de lignes à partir des résultats, mais cela ne réexécute pas la requête entière.
Keith
97

Personne n'a mentionné une différence cruciale, ironiquement, a répondu à une question fermée en double.

IEnumerable est en lecture seule et List ne l'est pas.

Voir Différence pratique entre List et IEnumerable

CAD bloke
la source
À titre de suivi, est-ce à cause de l'aspect Interface ou à cause de l'aspect Liste? autrement dit, IList est-il également en lecture seule?
Jason Masters
IList n'est pas en lecture seule - docs.microsoft.com/en-us/dotnet/api/… IEnumerable est en lecture seule car il ne dispose d'aucune méthode pour ajouter ou supprimer quoi que ce soit une fois qu'il est construit, c'est l'une des interfaces de base qui IList s'étend (voir lien)
CAD bloke
67

La chose la plus importante à réaliser est que, en utilisant Linq, la requête n'est pas évaluée immédiatement. Il n'est exécuté que dans le cadre d'une itération à travers le résultat IEnumerable<T>en un foreach- c'est ce que font tous les délégués étranges.

Ainsi, le premier exemple évalue la requête immédiatement en appelant ToListet en plaçant les résultats de la requête dans une liste.
Le deuxième exemple renvoie un IEnumerable<T>qui contient toutes les informations nécessaires pour exécuter la requête ultérieurement.

En termes de performances, la réponse est que cela dépend . Si vous avez besoin que les résultats soient évalués à la fois (par exemple, vous mutez les structures que vous interrogez plus tard, ou si vous ne voulez pas que l'itération sur la IEnumerable<T>prenne trop de temps), utilisez une liste. Sinon, utilisez un IEnumerable<T>. La valeur par défaut doit être d'utiliser l'évaluation à la demande dans le deuxième exemple, car elle utilise généralement moins de mémoire, sauf s'il existe une raison spécifique pour stocker les résultats dans une liste.

thecoop
la source
Salut et merci d'avoir répondu :: -). Cela a dissipé presque tous mes doutes. Une idée de pourquoi l'énumérable est "divisé" en "intérieur" et "extérieur"? Cela se produit lorsque j'inspecte l'élément en mode débogage / interruption via la souris. Est-ce peut-être la contribution de Visual Studio? Énumérer sur place et indiquer l'entrée et la sortie de l'Enum?
Axonn
5
C'est le Joinfaire, c'est le travail - intérieur et extérieur sont les deux côtés de la jointure. En règle générale, ne vous inquiétez pas de ce qui se trouve réellement dans le IEnumerables, car il sera complètement différent de votre code actuel. Ne vous inquiétez que de la sortie réelle lorsque vous itérez dessus :)
thecoop
40

L'avantage de IEnumerable est l'exécution différée (généralement avec des bases de données). La requête ne sera pas exécutée tant que vous n'aurez pas parcouru les données. C'est une requête en attente jusqu'à ce qu'elle soit nécessaire (aka chargement paresseux).

Si vous appelez ToList, la requête sera exécutée ou "matérialisée" comme j'aime à le dire.

Il y a des avantages et des inconvénients pour les deux. Si vous appelez ToList, vous pouvez supprimer un mystère quant au moment où la requête est exécutée. Si vous vous en tenez à IEnumerable, vous obtenez l'avantage que le programme ne fait aucun travail jusqu'à ce qu'il soit réellement requis.

Matt Sherman
la source
25

Je vais partager un concept mal utilisé dans lequel je suis tombé un jour:

var names = new List<string> {"mercedes", "mazda", "bmw", "fiat", "ferrari"};

var startingWith_M = names.Where(x => x.StartsWith("m"));

var startingWith_F = names.Where(x => x.StartsWith("f"));


// updating existing list
names[0] = "ford";

// Guess what should be printed before continuing
print( startingWith_M.ToList() );
print( startingWith_F.ToList() );

Résultat attendu

// I was expecting    
print( startingWith_M.ToList() ); // mercedes, mazda
print( startingWith_F.ToList() ); // fiat, ferrari

Résultat actuel

// what printed actualy   
print( startingWith_M.ToList() ); // mazda
print( startingWith_F.ToList() ); // ford, fiat, ferrari

Explication

Comme pour les autres réponses, l'évaluation du résultat a été différée jusqu'à l'appel ToListou des méthodes d'invocation similaires, par exempleToArray .

Je peux donc réécrire le code dans ce cas comme:

var names = new List<string> {"mercedes", "mazda", "bmw", "fiat", "ferrari"};

// updating existing list
names[0] = "ford";

// before calling ToList directly
var startingWith_M = names.Where(x => x.StartsWith("m"));

var startingWith_F = names.Where(x => x.StartsWith("f"));

print( startingWith_M.ToList() );
print( startingWith_F.ToList() );

Jouez autour

https://repl.it/E8Ki/0

amd
la source
1
C'est à cause des méthodes linq (extension) qui dans ce cas proviennent de IEnumerable où seulement créer une requête mais pas l'exécuter (en arrière-plan les arborescences d'expression sont utilisées). De cette façon, vous avez la possibilité de faire beaucoup de choses avec cette requête sans toucher aux données (dans ce cas, les données de la liste). La méthode List prend la requête préparée et l'exécute par rapport à la source de données.
Bronek
2
En fait, j'ai lu toutes les réponses, et la vôtre a été celle que j'ai votée, car elle indique clairement la différence entre les deux sans parler spécifiquement de LINQ / SQL. Il est essentiel de tout savoir AVANT d'arriver à LINQ / SQL. Admirer.
BeemerGuy
C'est une différence importante à expliquer, mais votre "résultat attendu" n'est pas vraiment attendu. Vous dites que c'est une sorte de piège plutôt que de design.
Neme
@Neme, oui C'était mon attente avant de comprendre comment ça IEnumerablemarche, mais maintenant ce n'est plus puisque je sais comment;)
amd
15

Si tout ce que vous voulez faire est de les énumérer, utilisez le IEnumerable.

Attention, cependant, changer la collection d'origine en cours d'énumération est une opération dangereuse - dans ce cas, vous voudrez d' ToListabord. Cela créera un nouvel élément de liste pour chaque élément en mémoire, énumérant le IEnumerableet est donc moins performant si vous ne l'énumérez qu'une seule fois - mais plus sûr et parfois les Listméthodes sont pratiques (par exemple en accès aléatoire).

Daren Thomas
la source
1
Je ne suis pas sûr qu'il soit sûr de dire que générer une liste signifie une baisse des performances.
Steven Sudit
@ Steven: en effet, comme l'ont dit thecoop et Chris, il peut parfois être nécessaire d'utiliser une liste. Dans mon cas, j'ai conclu que non. @ Daren: que voulez-vous dire par "cela créera une nouvelle liste pour chaque élément en mémoire"? Peut-être que vous vouliez dire une "entrée de liste"? :: -).
Axonn
@Axonn oui, je mentionne l'entrée de la liste. fixé.
Daren Thomas
@Steven Si vous prévoyez d'itérer sur les éléments dans le IEnumerable, puis en créant d'abord une liste (et en itérant dessus), vous devez itérer sur les éléments deux fois . Donc, à moins que vous ne souhaitiez effectuer des opérations plus efficaces sur la liste, cela signifie vraiment une baisse des performances.
Daren Thomas
3
@jerhewet: ce n'est jamais une bonne idée de modifier une séquence en cours d'itération. De mauvaises choses vont arriver. Les abstractions fuiront. Les démons feront irruption dans notre dimension et feront des ravages. Alors oui, .ToList()ça aide ici;)
Daren Thomas
5

En plus de toutes les réponses affichées ci-dessus, voici mes deux cents. Il existe de nombreux autres types que List qui implémente IEnumerable comme ICollection, ArrayList etc. Donc, si nous avons IEnumerable comme paramètre d'une méthode, nous pouvons transmettre n'importe quel type de collection à la fonction. C'est-à-dire que nous pouvons avoir une méthode pour opérer sur l'abstraction et non sur une implémentation spécifique.

Ananth
la source
1

Il existe de nombreux cas (comme une liste infinie ou une très grande liste) où IEnumerable ne peut pas être transformé en liste. Les exemples les plus évidents sont tous les nombres premiers, tous les utilisateurs de facebook avec leurs détails ou tous les articles sur ebay.

La différence est que les objets "List" sont stockés "ici et maintenant", alors que les "objets IEnumerable" fonctionnent "un par un". Donc, si je passe en revue tous les éléments sur ebay, un par un serait quelque chose que même un petit ordinateur peut gérer, mais ".ToList ()" me manquerait sûrement de mémoire, quelle que soit la taille de mon ordinateur. Aucun ordinateur ne peut à lui seul contenir et gérer une telle quantité de données.

[Modifier] - Inutile de dire que ce n'est pas "ni ceci ni cela". il serait souvent judicieux d'utiliser à la fois une liste et un IEnumerable dans la même classe. Aucun ordinateur au monde ne pourrait répertorier tous les nombres premiers, car par définition cela nécessiterait une quantité infinie de mémoire. Mais vous pourriez facilement penser à un class PrimeContainerqui contient un IEnumerable<long> primes, qui, pour des raisons évidentes, contient également un SortedList<long> _primes. tous les nombres premiers calculés jusqu'à présent. le prochain nombre premier à vérifier ne serait exécuté que par rapport aux nombres premiers existants (jusqu'à la racine carrée). De cette façon, vous obtenez les deux - les nombres premiers un à la fois (IEnumerable) et une bonne liste de "nombres premiers jusqu'à présent", ce qui est une assez bonne approximation de la liste entière (infinie).

LongChalk
la source