Chaque boucle foreach et chaque requête LINQ passe par une petite interface : IEnumerable<T>. La comprendre explique pourquoi LINQ est paresseux, pourquoi certaines séquences ne peuvent être lues qu'une fois, et comment yield return vous permet d'écrire vos propres séquences avec une simple boucle.
Ce qu'est IEnumerable
IEnumerable<T> a une seule méthode, GetEnumerator(). L'énumérateur renvoyé a MoveNext() (avancer, et indiquer s'il y a un élément) et Current (l'élément). Un foreach est un raccourci pour piloter cet énumérateur :
Sortie :
red green blue
red green blue
L'énumérateur démarre avant le premier élément, donc le premier MoveNext() se place dessus. Quand MoveNext() renvoie false, la séquence est terminée. Le bloc using libère l'énumérateur, ce qui compte pour les itérateurs qui détiennent des ressources comme des fichiers ouverts.
IEnumerable, ICollection ou List
Les interfaces de collection forment une échelle. Chaque échelon promet davantage :
| Type | Ajoute | Usage typique |
|---|---|---|
IEnumerable<T> | foreach seulement | Paramètres que vous ne faites que parcourir ; résultats LINQ ; séquences paresseuses |
IReadOnlyCollection<T> | Count | Valeurs de retour que l'appelant ne doit pas modifier |
ICollection<T> | Count, Add, Remove, Contains | Code qui ajoute à une collection de n'importe quel type |
IReadOnlyList<T> | Accès par index [i] | Résultats en lecture seule avec positions |
IList<T> | Accès par index, Insert, RemoveAt | Code qui modifie par position |
List<T> | La classe concrète qui implémente tout cela | Stockage |
Une méthode qui ne fait que parcourir son entrée devrait accepter IEnumerable<T>, pour que les appelants puissent passer un tableau, une liste, un ensemble ou une requête LINQ sans conversion. Une méthode qui renvoie des données devrait en général renvoyer quelque chose qui dit « ceci est déjà en mémoire », comme List<T> ou IReadOnlyList<T>, car une valeur de retour IEnumerable<T> pourrait être une requête qui refait le travail à chaque lecture.
Sortie :
30
15
4.0
30
Écrire un itérateur avec yield return
Pour produire votre propre séquence, écrivez une méthode qui renvoie IEnumerable<T> et utilisez yield return pour chaque élément. Le compilateur transforme la méthode en une classe qui implémente l'énumérateur pour vous.
Sortie :
0, 2, 4, 6, 8, 10
Mon Tue Wed Thu Fri
Il n'y a aucune liste dans EvenNumbers. Chaque yield return transmet une valeur à l'appelant et met la méthode en pause, en gardant ses variables locales (ici i) en vie jusqu'à ce que l'appelant demande la valeur suivante.
La paresse : observer l'ordre d'exécution
Comme la méthode se met en pause, son corps et la boucle de l'appelant s'exécutent à tour de rôle. Afficher des messages des deux côtés montre cet entrelacement :
Sortie :
Called Numbers(), nothing printed yet
start
yielding 1
Loop got 1
resumed after 1
yielding 2
Loop got 2
resumed after 2
yielding 3
Loop got 3
resumed after 3
end
Appeler Numbers() n'a pas affiché « start » : cela a seulement créé l'énumérable. Le corps a démarré au premier MoveNext() dans le foreach, s'est exécuté jusqu'au premier yield return, puis s'est arrêté. Chaque étape suivante a repris à la ligne qui suit le yield. Les Where et Select de LINQ se comportent de la même façon (le runtime les implémente avec des classes d'énumérateur écrites à la main pour la vitesse, mais l'effet est identique), c'est pourquoi les requêtes LINQ sont différées.
yield break
yield break termine la séquence plus tôt ; c'est la version itérateur de return :
Sortie :
Subject: Invoice
From: shop@example.com
Vous ne pouvez pas mélanger return value; et yield return dans une même méthode. Dès qu'une méthode contient yield, c'est un itérateur, et yield break est le seul moyen d'en sortir plus tôt.
Séquences infinies
Comme seuls les éléments demandés sont calculés, un itérateur peut décrire une séquence sans fin. C'est le consommateur qui décide combien en prendre :
Sortie :
0, 1, 1, 2, 3, 5, 8, 13, 21, 34
1597
2026-09-28 Mon
2026-10-05 Mon
2026-10-12 Mon
while (true) est sûr ici, car Take et First cessent de demander une fois qu'ils ont ce qu'il leur faut. Appeler ToList() ou Count() sur une séquence infinie, en revanche, ne se termine jamais, pas plus qu'énumérer un OrderBy sur elle, car le tri a d'abord besoin de tous les éléments.
Les pièges des séquences paresseuses
Les vérifications d'arguments s'exécutent tard. Tout le corps d'un itérateur est différé, y compris la validation placée en tête. La correction consiste à scinder la méthode : une méthode normale vérifie les arguments et renvoie le résultat d'un itérateur privé.
Sortie :
LazyRange(-1) returned without an error
...and threw only when enumerated
EagerRange(-1) threw at the call
Chaque énumération réexécute la méthode. Un itérateur ne met rien en cache. Boucler deux fois sur Numbers() affiche « start » deux fois, relit le fichier deux fois ou appelle le service web deux fois. Les IDE le signalent par « possible multiple enumeration of IEnumerable ». Appelez ToList() une fois quand vous avez besoin des éléments plus d'une fois.
finally s'exécute quand le consommateur s'arrête. Si l'appelant sort du foreach plus tôt, l'énumérateur est libéré et tout bloc finally de l'itérateur s'exécute ; c'est ainsi qu'un itérateur qui lit un fichier le referme :
Sortie :
open sensor
10
20
30
close sensor
done
Quelques règles que le compilateur impose aux itérateurs : yield return ne peut pas apparaître dans un bloc try qui a un catch (un try/finally convient), dans une lambda ou une méthode anonyme, ni dans une méthode qui a des paramètres ref ou out.
Flux asynchrones
Pour les séquences dont les éléments arrivent de façon asynchrone (pages d'une API, lignes d'une base de données), C# 8 et plus (avec .NET Core 3.0 ou plus) combinent les itérateurs et await via IAsyncEnumerable<T> :
static async IAsyncEnumerable<string> FetchPages()
{
for (int page = 1; page <= 3; page++)
{
await Task.Delay(100); // simulate a network call
yield return $"page {page}";
}
}
await foreach (string p in FetchPages())
{
Console.WriteLine(p);
}
Le consommateur utilise await foreach, et chaque élément est attendu au fur et à mesure de sa production.
Erreurs courantes
- Renvoyer
IEnumerable<T>depuis une requête et le lire de nombreuses fois. Chaque lecture la réexécute ; renvoyez plutôt uneList<T>. - Valider des arguments dans un itérateur. La vérification est différée ; scindez en une méthode d'enveloppe et un itérateur.
- Appeler
Count()ouToList()sur un itérateur infini. Cela ne se termine jamais ; appliquez d'abordTake. - S'attendre à ce qu'un paramètre
IEnumerable<T>aitCountou[i]. Utilisez leCount()de LINQ (qui peut énumérer) ou acceptezIReadOnlyList<T>.
Questions fréquentes
Qu'est-ce qu'IEnumerable en C# ?
IEnumerable<T> est l'interface de tout ce qui peut être parcouru avec foreach. Elle a une seule méthode, GetEnumerator(), qui renvoie un IEnumerator<T> doté de MoveNext() et Current. Les tableaux, listes, dictionnaires, ensembles, requêtes LINQ et méthodes itérateurs l'implémentent tous.
Quelle est la différence entre IEnumerable et List en C# ?
IEnumerable<T> promet seulement qu'on peut itérer ; il n'a ni propriété Count, ni indexeur, ni Add, et il peut être calculé de façon paresseuse à chaque boucle. List<T> est une collection concrète stockée en mémoire qui a tout cela. Acceptez IEnumerable<T> en paramètre quand vous ne faites que boucler, et renvoyez une List<T> ou un IReadOnlyList<T> quand les appelants ont besoin d'indexer ou de compter.
Que fait yield return en C# ?
yield return value;, dans une méthode qui renvoie IEnumerable<T>, transmet un élément à l'appelant et met la méthode en pause à cet endroit. La prochaine fois que l'appelant demande un élément, la méthode reprend à la ligne suivante. Le compilateur réécrit la méthode en machine à états : vous écrivez une boucle et obtenez une séquence paresseuse.
Qu'est-ce que yield break en C# ?
yield break; termine la séquence : le foreach de l'appelant se termine normalement après les éléments déjà produits. C'est l'équivalent de return pour un itérateur, utilisé pour s'arrêter plus tôt, par exemple quand une limite est atteinte ou que l'entrée est vide.
Pourquoi ma méthode itérateur ne lève-t-elle pas d'exception quand je l'appelle ?
Le corps d'une méthode itérateur ne s'exécute pas tant que la séquence n'est pas énumérée, donc les vérifications d'arguments placées en tête s'exécutent au premier MoveNext, pas à l'appel. Pour valider immédiatement, placez les vérifications dans une méthode publique normale qui appelle ensuite une méthode itérateur privée contenant les instructions yield.