Iterators
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