Décompiler yield return : comment les blocs itérateurs deviennent des machines à états
Une méthode itérateur avec yield return n'a pas de représentation IL directe — le compilateur la réécrit en une classe générée qui implémente IEnumerator et reprend à l'intérieur d'un switch MoveNext. Voici la forme de cette machine à états, comment un décompilateur la reconnaît, et comment yield return et yield break sont récupérés du code généré.
Certaines constructions C# compilent en une seule instruction IL. yield return compile en une classe entière. Il n”y a pas d”opcode « yield » dans le CLR, donc quand vous écrivez une méthode itérateur, le compilateur réécrit le tout en un type généré qui implémente IEnumerator<T> et se souvient d”où il en était entre les appels. Comprendre cette réécriture explique pourquoi un décompilateur vous montre parfois un cryptique <GetItems>d__0 avec un switch dedans — et comment un bon le reconvertit dans le yield return que vous avez réellement écrit.
La méthode que vous écrivez vs. la méthode qui est distribuée
Voici un itérateur ordinaire :
public IEnumerable<int> Evens(int max)
{
for (int i = 0; i <= max; i++)
if (i % 2 == 0)
yield return i;
}
Ce qui est distribué dans l”assemblage n”en est presque rien. La méthode Evens ne survit que comme un stub qui instancie une classe générée et la retourne. Le vrai travail migre dans un type imbriqué généré par le compilateur — nommé par convention <Evens>d__0 — qui implémente IEnumerable<int>, IEnumerator<int> et IDisposable. Le squelette décompilé ressemble à ceci :
[CompilerGenerated]
private sealed class <Evens>d__0 : IEnumerable<int>, IEnumerator<int>, IDisposable
{
private int <>1__state; // where we are in the method
private int <>2__current; // the value the last yield produced
public int max; // the parameter, hoisted to a field
private int <i>5__1; // the local 'i', hoisted to a field
int IEnumerator<int>.Current => <>2__current;
bool IEnumerator.MoveNext() { /* the rewritten body — see below */ }
// ... Reset/Dispose/GetEnumerator ...
}
Deux sortes de champs apparaissent. <>1__state et <>2__current sont la machinerie : le champ d”état enregistre où reprendre, et le champ current contient la valeur que retourne Current. Les autres — max et <i>5__1 — sont votre paramètre et votre locale i, hissés de la pile vers le tas. Ils doivent être des champs, car une locale sur la pile disparaîtrait dès que MoveNext retournerait, et l”itérateur a besoin que i soit encore là au prochain appel.
MoveNext : un switch qui reprend
Le corps de votre itérateur est réécrit en MoveNext, structuré comme une répartition sur <>1__state. Chaque yield return devient trois étapes — ranger la valeur, enregistrer un état de reprise, retourner true — et le case correspondant est là où l”appel suivant resaute :
bool IEnumerator.MoveNext()
{
switch (<>1__state)
{
case 0: // first call: start the method
<>1__state = -1;
<i>5__1 = 0; // i = 0
break;
case 1: // resume point after the yield
<>1__state = -1;
<i>5__1++; // the i++ from the for-loop
break;
default:
return false;
}
while (<i>5__1 <= max)
{
if (<i>5__1 % 2 == 0)
{
<>2__current = <i>5__1; // yield return i → stash value
<>1__state = 1; // remember where to resume
return true; // and hand control back
}
<i>5__1++;
}
return false; // falling off the end = yield break
}
Lisez-le comme un programme reprenable. L”état 0 est l”entrée ; la méthode tourne jusqu”au premier yield return, qui fixe <>2__current, met l”état à 1 et retourne true. Le foreach qui consomme cet itérateur rappelle MoveNext, le switch voit l”état 1 et resaute après le yield pour poursuivre la boucle. Quand la boucle se termine enfin, MoveNext retourne false, ce qui est exactement ce en quoi compile yield break (et le fait de tomber de la fin) — le signal au foreach que l”énumération est finie.
Comment le décompilateur remet le yield
Un décompilateur qui lit l”assemblage distribué voit la classe générée, pas votre méthode. Récupérer l”itérateur est de la reconnaissance de motif sur une forme bien définie :
- Identifier la machine à états. Un type imbriqué
[CompilerGenerated]implémentantIEnumerator<T>avec un champ<>1__stateet un<>2__currentest la signature d”un bloc itérateur — distinct d”une machine à états async, qui implémenteIAsyncStateMachineet est pilotée par des awaiters plutôt que parMoveNext. - Dé-hisser les locales. Des champs comme
<i>5__1sont remappés vers une localei, et les champs correspondant aux paramètres d”origine (max) sont reconnus comme paramètres de la méthode reconstruite. L”encodage<nom>5__nrend l”identifiant d”origine récupérable. - Replier le switch en flux de contrôle. Chaque
casedans la répartition deMoveNextest un point de reprise qui correspond à un site deyield return. Le décompilateur recoud le code d”avant le yield et celui d”après la reprise en un corps linéaire, reconvertissant<>2__current = x; <>1__state = n; return true;enyield return x;et lereturn false;final en leyield breakimplicite.
Le résultat est à nouveau votre méthode Evens — une boucle for avec un yield return i dedans — au lieu d”une machine à états de 60 lignes. Glass.NET fait cette reconstruction par défaut et vous laisse descendre à la vue brute <Evens>d__0 quand vous voulez voir la machinerie générée réelle, ce qui est exactement là où vous regardez quand un itérateur se comporte mal.
Pourquoi cela vaut la peine de le savoir
Le yield return reconstruit est ce que vous voulez presque toujours, mais la machine à états en dessous explique des comportements que la vue de haut niveau cache. L”exécution différée devient évidente : la méthode stub ne fait que construire la classe, donc aucun code du corps de l”itérateur ne tourne avant le premier MoveNext — c”est pourquoi une exception lancée « dans » un itérateur ne surgit pas avant que vous commenciez à énumérer. L”état par énumérateur est visible comme des champs d”instance, ce qui explique pourquoi deux boucles foreach sur le même itérateur obtiennent chacune leur propre parcours indépendant. Et quand vous lisez la bibliothèque compilée de quelqu”un d”autre — ou la vôtre sans symboles — reconnaître <...>d__ avec une paire état/current vous dit instantanément que vous regardez un itérateur, et les étiquettes case dans MoveNext se mappent une à une avec les points yield de l”original. Un décompilateur qui connaît le motif vous donne le source lisible ; connaître le motif vous-même vous laisse lire la machine quand cela compte.
Essayez Nebula.NET
Renforcez votre code .NET en quelques minutes — commencez avec l'édition gratuite.