Decompilar yield return: cómo los bloques iteradores se vuelven máquinas de estado
Un método iterador con yield return no tiene representación directa en IL — el compilador lo reescribe en una clase generada que implementa IEnumerator y reanuda dentro de un switch MoveNext. Aquí tienes la forma de esa máquina de estado, cómo la reconoce un decompilador, y cómo se recuperan yield return y yield break del código generado.
Algunas construcciones de C# compilan a una única instrucción IL. yield return compila a una clase entera. No hay opcode «yield» en el CLR, así que cuando escribes un método iterador, el compilador reescribe todo en un tipo generado que implementa IEnumerator<T> y recuerda dónde estaba entre llamadas. Entender esa reescritura explica por qué un decompilador a veces te muestra un críptico <GetItems>d__0 con un switch dentro — y cómo uno bueno lo convierte de vuelta en el yield return que realmente escribiste.
El método que escribes frente al método que se distribuye
Aquí hay un iterador corriente:
public IEnumerable<int> Evens(int max)
{
for (int i = 0; i <= max; i++)
if (i % 2 == 0)
yield return i;
}
Lo que se distribuye en el ensamblado es casi nada de eso. El método Evens sobrevive solo como un stub que crea una clase generada y la devuelve. El trabajo real se traslada a un tipo anidado generado por el compilador — convencionalmente llamado <Evens>d__0 — que implementa IEnumerable<int>, IEnumerator<int> e IDisposable. El esqueleto decompilado se ve así:
[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 ...
}
Aparecen dos clases de campos. <>1__state y <>2__current son la maquinaria: el campo de estado registra dónde reanudar, y el campo current contiene el valor que devuelve Current. Los demás — max y <i>5__1 — son tu parámetro y tu local i, elevados de la pila al montículo. Tienen que ser campos, porque un local en la pila desaparecería en cuanto MoveNext devolviera, y el iterador necesita que i siga ahí en la siguiente llamada.
MoveNext: un switch que reanuda
El cuerpo de tu iterador se reescribe en MoveNext, estructurado como un despacho sobre <>1__state. Cada yield return se convierte en tres pasos — guardar el valor, registrar un estado de reanudación, devolver true — y el case correspondiente es donde la siguiente llamada salta de vuelta:
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
}
Léelo como un programa reanudable. El estado 0 es la entrada; el método corre hasta el primer yield return, que fija <>2__current, pone el estado en 1 y devuelve true. El foreach que consume este iterador llama a MoveNext otra vez, el switch ve el estado 1 y salta de vuelta después del yield para continuar el bucle. Cuando el bucle por fin termina, MoveNext devuelve false, que es exactamente a lo que compila yield break (y caerse del final) — la señal a foreach de que la enumeración ha terminado.
Cómo el decompilador vuelve a poner el yield
Un decompilador que lee el ensamblado distribuido ve la clase generada, no tu método. Recuperar el iterador es reconocimiento de patrones sobre una forma bien definida:
- Identificar la máquina de estado. Un tipo anidado
[CompilerGenerated]que implementaIEnumerator<T>con un campo<>1__statey uno<>2__currentes la firma de un bloque iterador — distinto de una máquina de estado async, que implementaIAsyncStateMachiney la impulsan awaiters en lugar deMoveNext. - Des-elevar las locales. Campos como
<i>5__1se mapean de vuelta a un locali, y los campos que coinciden con los parámetros originales (max) se reconocen como parámetros del método reconstruido. La codificación<nombre>5__nhace recuperable el identificador original. - Plegar el switch en flujo de control. Cada
caseen el despacho deMoveNextes un punto de reanudación que corresponde a un sitio deyield return. El decompilador cose el código previo al yield y el posterior a la reanudación de vuelta en un cuerpo lineal, convirtiendo<>2__current = x; <>1__state = n; return true;de nuevo enyield return x;y elreturn false;del final en elyield breakimplícito.
El resultado es tu método Evens de nuevo — un bucle for con un yield return i dentro — en lugar de una máquina de estado de 60 líneas. Glass.NET hace esta reconstrucción por defecto y te deja bajar a la vista cruda <Evens>d__0 cuando quieres ver la maquinaria generada real, que es exactamente donde miras cuando un iterador se porta mal.
Por qué vale la pena saberlo
El yield return reconstruido es lo que quieres casi siempre, pero la máquina de estado de debajo explica comportamientos que la vista de alto nivel oculta. La ejecución diferida se vuelve obvia: el método stub solo construye la clase, así que ningún código del cuerpo del iterador corre hasta el primer MoveNext — por lo que una excepción lanzada «en» un iterador no aflora hasta que empiezas a enumerar. El estado por enumerador es visible como campos de instancia, lo que explica por qué dos bucles foreach sobre el mismo iterador obtienen cada uno su propio recorrido independiente. Y cuando lees la biblioteca compilada de otra persona — o la tuya sin símbolos — reconocer <...>d__ con un par estado/current te dice al instante que estás viendo un iterador, y las etiquetas case en MoveNext se mapean uno a uno con los puntos yield del original. Un decompilador que conoce el patrón te da el código legible; conocer el patrón tú mismo te deja leer la máquina cuando importa.
Prueba Nebula.NET
Endurece tu código .NET en minutos — empieza con la edición gratuita.