Le yield
mot-clé vous permet de créer un IEnumerable<T>
dans le formulaire sur un bloc itérateur . Ce bloc itérateur prend en charge l' exécution différée et si vous n'êtes pas familier avec le concept, il peut sembler presque magique. Cependant, à la fin de la journée, c'est juste du code qui s'exécute sans astuces étranges.
Un bloc itérateur peut être décrit comme du sucre syntaxique où le compilateur génère une machine d'état qui garde une trace de la progression de l'énumération de l'énumérable. Pour énumérer un énumérable, vous utilisez souvent une foreach
boucle. Cependant, une foreach
boucle est également du sucre syntaxique. Vous êtes donc deux abstractions retirées du vrai code, c'est pourquoi il peut être difficile au début de comprendre comment tout cela fonctionne ensemble.
Supposons que vous ayez un bloc d'itérateur très simple:
IEnumerable<int> IteratorBlock()
{
Console.WriteLine("Begin");
yield return 1;
Console.WriteLine("After 1");
yield return 2;
Console.WriteLine("After 2");
yield return 42;
Console.WriteLine("End");
}
Les vrais blocs d'itérateur ont souvent des conditions et des boucles, mais lorsque vous vérifiez les conditions et déroulez les boucles, ils finissent toujours comme des yield
instructions entrelacées avec un autre code.
Pour énumérer le bloc itérateur, une foreach
boucle est utilisée:
foreach (var i in IteratorBlock())
Console.WriteLine(i);
Voici la sortie (pas de surprise ici):
Commencer
1
Après 1
2
Après 2
42
Fin
Comme indiqué ci foreach
- dessus est le sucre syntaxique:
IEnumerator<int> enumerator = null;
try
{
enumerator = IteratorBlock().GetEnumerator();
while (enumerator.MoveNext())
{
var i = enumerator.Current;
Console.WriteLine(i);
}
}
finally
{
enumerator?.Dispose();
}
Pour tenter de démêler cela, j'ai créé un diagramme de séquence avec les abstractions supprimées:
La machine à états générée par le compilateur implémente également l'énumérateur, mais pour rendre le diagramme plus clair, je les ai présentées comme des instances distinctes. (Lorsque la machine d'état est énumérée à partir d'un autre thread, vous obtenez en fait des instances distinctes mais ce détail n'est pas important ici.)
Chaque fois que vous appelez votre bloc itérateur, une nouvelle instance de la machine d'état est créée. Cependant, aucun de votre code dans le bloc itérateur n'est exécuté jusqu'à ce qu'il enumerator.MoveNext()
s'exécute pour la première fois. Voici comment fonctionne l'exécution différée. Voici un exemple (plutôt idiot):
var evenNumbers = IteratorBlock().Where(i => i%2 == 0);
À ce stade, l'itérateur n'a pas exécuté. La Where
clause crée un nouveau IEnumerable<T>
qui enveloppe le IEnumerable<T>
retourné par IteratorBlock
mais cet énumérable n'a pas encore été énuméré. Cela se produit lorsque vous exécutez une foreach
boucle:
foreach (var evenNumber in evenNumbers)
Console.WriteLine(eventNumber);
Si vous énumérez l'énumérable deux fois, une nouvelle instance de la machine d'état est créée à chaque fois et votre bloc itérateur exécutera le même code deux fois.
Notez que les méthodes de LINQ aiment ToList()
, ToArray()
, First()
, Count()
etc. utilisera une foreach
boucle pour énumérer les dénombrable. Par exemple ToList()
, énumérera tous les éléments de l'énumérable et les stockera dans une liste. Vous pouvez maintenant accéder à la liste pour obtenir tous les éléments de l'énumérable sans que le bloc d'itérateur ne s'exécute à nouveau. Il existe un compromis entre l'utilisation du processeur pour produire les éléments de l'énumération plusieurs fois et la mémoire pour stocker les éléments de l'énumération pour y accéder plusieurs fois lors de l'utilisation de méthodes telles que ToList()
.