Skip to content
← Todas las publicaciones
· Delta1 Labs Decompilación.NETAnálisis a fondo

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.

estado 0entraryield return icurrent=i; state=1return true →estado 1reanuda tras yieldbucle: siguiente MoveNext()return falseyield break / fin

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:

  1. Identificar la máquina de estado. Un tipo anidado [CompilerGenerated] que implementa IEnumerator<T> con un campo <>1__state y uno <>2__current es la firma de un bloque iterador — distinto de una máquina de estado async, que implementa IAsyncStateMachine y la impulsan awaiters en lugar de MoveNext.
  2. Des-elevar las locales. Campos como <i>5__1 se mapean de vuelta a un local i, 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__n hace recuperable el identificador original.
  3. Plegar el switch en flujo de control. Cada case en el despacho de MoveNext es un punto de reanudación que corresponde a un sitio de yield 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 en yield return x; y el return false; del final en el yield break implí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.