Menu

IEnumerable et yield return en C# : les séquences paresseuses expliquées

IEnumerable<T> est l'interface derrière foreach et LINQ, et yield return est la façon la plus simple de l'implémenter. Apprenez le lien entre IEnumerable, ICollection et List, comment une méthode itérateur s'exécute pas à pas, yield break, les séquences infinies et les pièges de l'évaluation paresseuse.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

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 :

TypeAjouteUsage typique
IEnumerable<T>foreach seulementParamètres que vous ne faites que parcourir ; résultats LINQ ; séquences paresseuses
IReadOnlyCollection<T>CountValeurs de retour que l'appelant ne doit pas modifier
ICollection<T>Count, Add, Remove, ContainsCode 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, RemoveAtCode qui modifie par position
List<T>La classe concrète qui implémente tout celaStockage

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 une List<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() ou ToList() sur un itérateur infini. Cela ne se termine jamais ; appliquez d'abord Take.
  • S'attendre à ce qu'un paramètre IEnumerable<T> ait Count ou [i]. Utilisez le Count() de LINQ (qui peut énumérer) ou acceptez IReadOnlyList<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.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER