Iterators

1/19/2011 6:16:00 PM By Felipe Pessoto

Muito antigamente, quando se programava em C# 1.0, caso você precisasse implementar as interfaces IEnumerable ou IEnumerator era necessário fazer na mão, implementando os métodos Current, MoveNext e Dispose para o IEnumerator e GetEnumerator para o IEnumerable.
O mais complicado era o MoveNext(), pois é necessário criar uma máquina de estado para saber qual resultado deve ser atribuido ao Current.
Estas interfaces são conceitos importantes no C# e na plataforma .NET em geral, o foreach funciona baseado na classe IEnumerable, chamando seu único método GetEnumerator que por sua vez retorna um object que implementa a interface IEnumerator.

Com o C# 2.0, uma nova keyword e muito trabalho do compilador, é possível simplificar tudo isso. Usando o yield return no seu método ou no get da propriedade o compilador se encarrega de criar uma classe aninhada que implementa essas interfaces pra você, controlando todos os fluxos respeitando if´s, for´s blocos finally, etc. É um trabalho enorme por parte do compilador e claro da equipe do compilador do C# que trabalhou duro no algoritmo. Hoje essa "feature" é uma das maiores no compilador do C#.

Nada melhor do que vermos o código para entender melhor como tudo isto funciona. Vou mostrar um método que retorna IEnumerator<int> e o resultado gerado pelo compilador, usando o Reflector pra isso.

Preciso apenas declarar o retorno do método como IEnumerator<int> e usar o yield return. Perceba que retorno int´s apesar do retorno ser IEnumerator<int>, isso é possível pois o compilador faz a tradução pra gente:

public IEnumerator<int> RetornarEnumeratorSimples()
{
    yield return 1;
    yield return 6;
    yield return 4;
}

Depois de compilado:

public IEnumerator<int> RetornarEnumeratorSimples()
{
    return new <RetornarEnumeratorSimples>d__0(0) { <>4__this = this };
}

A classe que é retornada é do tipo <RetornarEnumeratorSimples>d__0, o compilador gera esse nome estranho pra evitar conflito com as nossas classes, usando caracteres inválidos. São adicionados alguns Attributes para sinalizar que é um código gerado pelo compilador e evitar que o Debugger entre nesse código.

O código da classe que foi gerada:

[CompilerGenerated]
private sealed class <RetornarEnumeratorSimples>d__0 : IEnumerator<int>, IEnumerator, IDisposable
{
    // Fields
    private int <>1__state;
    private int <>2__current;
    public Iterator <>4__this;

    // Methods
    [DebuggerHidden]
    public <RetornarEnumeratorSimples>d__0(int <>1__state)
    {
        this.<>1__state = <>1__state;
    }

    private bool MoveNext()
    {
        switch (this.<>1__state)
        {
            case 0:
                this.<>1__state = -1;
                this.<>2__current = 1;
                this.<>1__state = 1;
                return true;

            case 1:
                this.<>1__state = -1;
                this.<>2__current = 6;
                this.<>1__state = 2;
                return true;

            case 2:
                this.<>1__state = -1;
                this.<>2__current = 4;
                this.<>1__state = 3;
                return true;

            case 3:
                this.<>1__state = -1;
                break;
        }
        return false;
    }

    [DebuggerHidden]
    void IEnumerator.Reset()
    {
        throw new NotSupportedException();
    }

    void IDisposable.Dispose()
    {
    }

    // Properties
    int IEnumerator<int>.Current
    {
        [DebuggerHidden]
        get
        {
            return this.<>2__current;
        }
    }

    object IEnumerator.Current
    {
        [DebuggerHidden]
        get
        {
            return this.<>2__current;
        }
    }
}

Nosso método RetornarEnumeratorSimples instancia o <RetornarEnumeratorSimples>d__0 passando 0 no construtor, que é atribuido ao state. A mágica mesmo acontece mesmo no MoveNext que usando a variavel <>1__state sabe a partir de que parte do código deve continuar. Nosso método original é bem simples, e gerou uma boa quantidade de código. Fazer isso manualmente é perigoso, sendo muito fácil de se cometer erros. Vamos ver um caso um pouco mais complexo, desta vez retornando um IEnumerable<int>:

public IEnumerable<int> RetornarEnumerable(string numero)
{
    try
    {
        yield return 1;
        var d = 4;
        if (++d == 5)
        {
            yield return int.Parse(numero);
        }
        yield return (int)this.VarPublica;
    }
    finally
    {
        Console.WriteLine("Finally");
    }
}
Depois de compilado:
public IEnumerable<int> RetornarEnumerable(string numero)
{
    return new <RetornarEnumerable>d__8(-2) { <>4__this = this, <>3__numero = numero };
}

O fato de retornarmos um IEnumerable em vez de IEnumerator faz o compilador criar um método adicionar, pra implementar a interface IEnumerable, o GetEnumerator. Agora a classe gerada implementará tanto o IEnumerable quanto o IEnumerator:

[CompilerGenerated]
private sealed class <RetornarEnumerable>d__8 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator, IDisposable
{
    // Fields
    private int <>1__state;
    private int <>2__current;
    public string <>3__numero;
    public Iterator <>4__this;
    private int <>l__initialThreadId;
    public int <d>5__9;
    public string numero;

    // Methods
    [DebuggerHidden]
    public <RetornarEnumerable>d__8(int <>1__state)
    {
        this.<>1__state = <>1__state;
        this.<>l__initialThreadId = Thread.CurrentThread.ManagedThreadId;
    }

    private void <>m__Finallya()
    {
        this.<>1__state = -1;
        Console.WriteLine("Finally");
    }

    private bool MoveNext()
    {
        bool CS$1$0000;
        try
        {
            switch (this.<>1__state)
            {
                case 0:
                    this.<>1__state = -1;
                    this.<>1__state = 1;
                    this.<>2__current = 1;
                    this.<>1__state = 2;
                    return true;

                case 2:
                    this.<>1__state = 1;
                    this.<d>5__9 = 4;
                    if (++this.<d>5__9 != 5)
                    {
                        goto Label_008B;
                    }
                    this.<>2__current = int.Parse(this.numero);
                    this.<>1__state = 3;
                    return true;

                case 3:
                    break;

                case 4:
                    this.<>1__state = 1;
                    this.<>m__Finallya();
                    goto Label_00B5;

                default:
                    goto Label_00B5;
            }
            this.<>1__state = 1;
        Label_008B:
            this.<>2__current = (int) this.<>4__this.VarPublica;
            this.<>1__state = 4;
            return true;
        Label_00B5:
            CS$1$0000 = false;
        }
        fault
        {
            this.System.IDisposable.Dispose();
        }
        return CS$1$0000;
    }

    [DebuggerHidden]
    IEnumerator<int> IEnumerable<int>.GetEnumerator()
    {
        Iterator.<RetornarEnumerable>d__8 d__;
        if ((Thread.CurrentThread.ManagedThreadId == this.<>l__initialThreadId) && (this.<>1__state == -2))
        {
            this.<>1__state = 0;
            d__ = this;
        }
        else
        {
            d__ = new Iterator.<RetornarEnumerable>d__8(0) {
                <>4__this = this.<>4__this
            };
        }
        d__.numero = this.<>3__numero;
        return d__;
    }

    [DebuggerHidden]
    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator();
    }

    [DebuggerHidden]
    void IEnumerator.Reset()
    {
        throw new NotSupportedException();
    }

    void IDisposable.Dispose()
    {
        switch (this.<>1__state)
        {
            case 1:
            case 2:
            case 3:
            case 4:
                try
                {
                }
                finally
                {
                    this.<>m__Finallya();
                }
                return;
        }
    }

    // Properties
    int IEnumerator<int>.Current
    {
        [DebuggerHidden]
        get
        {
            return this.<>2__current;
        }
    }

    object IEnumerator.Current
    {
        [DebuggerHidden]
        get
        {
            return this.<>2__current;
        }
    }
}

O código gerado agora é um pouco maior que o anterior, em parte por retornarmos IEnumerable em parte por termos um método mais complexo. Para implementar a interface IEnumerable temos um novo método GetEnumerator(na verdade temos dois, pois um implementa a versão da interface que não é Generic, apenas chamando a versão que usa Generic), este método sempre cria uma nova instancia da classe, e configura as variaveis <>4__this e a numero. Isso é para garantir que cada vez que o GetEnumerator seja chamado, sempre retorna uma nova instancia e com as variaveis no estado inicial, permitindo criar vários foreach´s a partir de um mesmo objeto, sem um interferir no outro. O valor inicial do numero é guardado na variavel <>3__numero e é atribuido inicialmente na chamada do método RetornarEnumerable. Para o this não precisamos de uma variável adicional para guardar o valor original, pois não é possível alterar o this de uma classe, então o método sempre atribui o this que ele mesmo guarda: <>4__this = this.<>4__this, a exceção é no caso de structs, onde ele cria uma variavel <>3__<>4__this que guarda o this original.
O GetEnumerator tem uma otimização, caso ele seja chamado pela mesma Thread que instanciou a class e o <>1__state seja -2, significando que é a primeira vez que este método é chamado para esta instancia, assim ele retorna a própria instancia, já que nossa classe alem de IEnumerable também é IEnumerator, evitando criar outro objeto. Na maioria dos casos é o que acontece, criamos uma classe, e fazemos um foreach logo em seguida, na mesma Thread.

A variável <>1__state segue um padrão, sendo -2 caso o GetEnumerator ainda não tenha sido chamado, -1 caso esteja em execução, 0 antes de chamar o MoveNext pela primeira vez e qualquer número positivo para indicar a partie de onde o MoveNext deve continuar

Além do código gerado para suportar o GetEnumerator nossa classe tem alguns detalhes a mais, o método <>m__Finallya() que contem o código que colocamos no finally, este código é chamado no MoveNext quando não temos mais itens e no Dispose. O Dispose é chamado pelo bloco foreach ao termino dele, e também no bloco fault do MoveNext caso ocorra algum erro. O fault não pode ser usado diretamente no C#, ele se comporta como um catch com throw no final, executando o nosso código e relançando nossa Exception. Nos Iterator não podemos usar try catch, apenas try finally.

Da pra perceber quanto trabalho o compilador nos poupou e mais importante é garantido que funciona(pelo menos a parte dele), um código tão repetitivo e cheio de regras de fluxo seria bem complexo de se criar e manter. É um trabalho realmente para máquinas.

Tudo isto está disponível desde o C# 2.0, caso você precise usar no .NET 1.0 acho que o melhor a se fazer é criar o código no .NET 2.0, compilar e copiar o código gerado pro .NET 1.0


Comments (0)