Article Details                  
 
Novidades C# - parte IV (blocos iterators)

Neste último artigo da série, apresento algumas caracteristicas importantes relacionadas com os iterators.

Novidades C# 2.0 - parte IV (Blocos iterators)


Autor: Luís Abreu

Conteúdo: Novidades existentes na nova framework 2.0 (
Blocos iterators )

Ferramentas: Visual C# Express


Presumo eu que podemos considerar que toda a gente conhece o termo foreach. Para os que não conhecem este termo, fica aqui uma apresentação rápida: permite percorrer todos os elementos de uma colecção de forma rápida e indolor! Na prática, este tipo de expressões apenas pode ser aplicado às colecções que implementam o interface IEnumerable (ou, que simplesmente contenham o método GetEnumerator definido por esse interface).


Agora uma pergunta: como é que fazemos (na versão 1.x) para que uma colecção possa ser utilizada num ciclo controlado pelo termo foreach? Resposta rápida: a maneira mais fácil consiste em guardarmos os elementos na nossa colecção num ArrayList e delegar a implementação dos interfaces necessários para esse elemento! A utilização deste tipo de estratégias tem apenas uma justificação: implementar um IEnumerator é uma seca! (já vamos ver porquê ao longo deste artigo).


Para nossa felicidade, a versão 2.0 do C# já permite utilizarmos os chamados blocos iterators, que facilitam e ajudam (MUITO) na implementação de IEnumerators.


IEnumerable, IEnumerator...e que confusão!


Parece-me a mim que, antes de apresentarmos os novos iterators e suas regras, convém refrescarmos a memória e recapitularmos as principais funções associadas a cada um dos interfaces anteriores. Assim, de uma forma resumida podemos afirmar que:



  • o interface IEnumerable permite obter um interface IEnumerator, ou seja, funciona como um factory de IEnumerators;

  • por outro lado, o interface IEnumerator permite a iteracção simples ao longo da colecção.


Como seria de esperar, o interface IEnumerable apenas disponibiliza um método:


 
interface IEnumerable
{
IEnumerator GetEnumerator();
}


Por sua vez, o interface IEnumerable já é mais interessante a nível de membros:



interface IEnumerator
{
object Current
{
get;
}

bool MoveNext();
void Reset();
}


Portanto, se tivéssemos uma colecção que implementasse o interface IEnumerable, então poderíamos utilizar o seguinte código para percorrer essa colecção:



TestColl t = new TestColl(); //automaticamente inicializa a colecção com inteiros...
foreach( int a in t )
{
Console.WriteLine( a );
}

Se gostarem de escrever, então podem substituir a lógica anterior por esta:



IEnumerator aux = t.GetEnumerator( );
while ( aux.MoveNext( ) )
{
Console.WriteLine( aux.Current.ToString( ) );
}
Console.Read( );

A implementação do interface IEnumerator não é uma tarefa simples. Se não acreditam, então consultem a documentação. Para começar, existe uma regra de ouro: a utilização de enumerators (vou passar a designar por todos os objectos que implementam o interface IEnumerator de enumerators) não deve modificar a colecção. Por outras palavras, não os podemos utilizar para adicionar novos itens a uma colecção ou para remover elementos existentes.


Quando obtemos um enumerator, este está sempre num estado que vamos designar por inicial. Ao evocarmos o método Reset, fazemos com que o enumerator volte a esse estado. Quando estamos neste estado inicial, não podemos aceder à propriedade Current. Ou melhor, se acedermos é bom estarmos preparados para a excepção que vai ser gerada. Se quisermos avançar para o primeiro elemento da colecção (ou então se quisermos avançar para outro elemento), temos de recorrer ao método MoveNext. Este método retorna true, se o valor Current for actualizado para um novo elemento; por outro lado, retorna false se tal não acontecer (ou seja, se já estivermos para lá do último elemento). Caso retorne true, o iterator passa a estar inicializado.


Convém ainda referir que um enumerator só é válido enquanto a colecção a partir do qual ele foi obtido não for alterada. Presumo que já os consegui convencer a todos que a implementação deste tipo de objectos não é muito fácil.


Blocos Iterator


Bom, agora que já apresentámos alguns dos principais principios que temos de ter em atenção aquando da implementação do interface IEnumerator, vamos então falar desta novidade da versão 2.0. À semelhança do que acontece com, por exemplo, os métodos anónimos, a utilização de blocos iterator simplifica a escrita do código necessário. Na prática, o compilador irá ser responsável por transformar as instruções contidas no bloco em IL de forma a que seja construída uma classe que implemente o interface IEnumerator. Ou seja, se no caso dos métodos anónimos o compilador gerava código que automaticamente relacionava um evento(ou delegate) com um determinado método (ou classe) que era gerado de forma automática, aqui o compilador vai introduzir automaticamente uma nova classe que irá implementar o interface em causa. Antes de falarmos acerca dos pormenores, vamos apresentar um exemplo prático da utilização deste tipo de blocos.



public IEnumerator GetEnumerator( )
{
for ( int i = 0; i < _arr.Length; i++ )
yield return _arr [i];
}

Os blocos iterator distinguem-se dos outros blocos devido à utilização das expressões yield return ou yield break. A expressão yield return devolve o próximo valor da enumeração; por outro lado, a expressão yield break indica que a iteracção está completa. Um bloco deste tipo apenas pode ser utilizado como corpo de uma função, de um operador ou como implementação de uma propriedade (mais concretamente na secção de leitura) desde que esses elementos retornem um interface do tipo IEnumerator ou IEnumerable.


A utilização de genéricos aumenta o scope desses interfaces. Por outras palavras, uma vez que a nova versão da framework permite a uilização de generics, então os interfaces do tipo IEnumerator passam a ser os interfaces do tipo IEnumerator e IEnumerator<T> (o mesmo raciociono deve ser seguido em relação aos interfaces do tipo IEnumerable).


A utilização deste tipo de blocos introduz algumas subtilezas não aparentes (e que, como é óbvio, iremos apresentar até ao final deste texto). Assim, quando um método contém um destes blocos não pode receber parâmetros do tipo ref ou out. Para além disso, não é possível termos apenas a instrução return no interior de um bloco deste tipo. Esta expressão tem de ser utilizada juntamente com o termo yield.


Um bloco yield devolve sempre um valor do mesmo tipo. Tecnicamente, este valor é designado de yield type. Este tipo depende do tipo de interface associado ao bloco. Se estivermos a falar de IEnumerator ou IEnumerable, então o tipo de associado é sempre object. Se estivermos a falar de IEnumerator<T> ou IEnumerable<T>, então o tipo associado é T.


Expressões yield


Como foi afirmado anteriormente, um bloco iterator obtém o próximo valor através da expressão yield. Existem várias restrições que devem de ser tidas em conta na utilização deste tipo de expressões:



  • expressões yield (yield return ou yield break) não podem ser utilizadas no interior de métodos anónimos;

  • expressões yield não podem aparecer na secção de código associada ao finally;

  • as expressões yield return também não podem ser associadas a um bloco try que contenha um bloco catch associado (nota: as expressões yield break podem estar contidas em blocos deste tipo).


Os exemplos seguintes ilustram estas regras:




IEnumerator GetEnumerator()
{
try
{
yield return "Ola"; //ok
yield break; //OK
}
finally
{
yield return "Error"; //erro: yield return em finally
yield break; //erro: yield break em finally
}
}

IEnumerator GetEnumerator()
{
try
{
yield return "Erro"; //erro: yield return com catch associado!
yield break; //ok
}
catch
{
yield return "Mais erros"; //erro: yield return em bloco catch
yield break; //ok
}
}

delegate IEnumerable Test ();
Test a = delegate{yield return "Erro";} //yield utilizado em metodo anonimo


Pormenores relevantes


Quando utilizamos uma expressão yield return, temos de garantir que existe uma conversão implicita para o tipo de retorno do enumerator. A execução de uma instrução yield return é, no mínimo, interessante. Senão, vejamos:



  • a expressão associada é analisada e convertida para o tipo de retorno (como vimos, este tipo de retorno depende de estarmos ou não a utilizar um genérico);

  • o valor calculado no item anterior é atribuido à propriedade Current do enumerator;

  • o processamento do bloco associado ao iterator é suspenso; se a expressão yield return estiver contida num bloco try...finally, as instruções contidas no bloco finally não são executadas nesta altura;

  • o método MoveNext (que foi evocado pelo cliente para desencadear esta sequência de acções) retorna true, indicando assim que ainda existem mais itens para serem mostrados.

  • A próxima evocação do método MoveNext (efectuada pelo código cliente) retoma a execução do iterator a partir do estado em que tinha ficado na última evocação do MoveNext.


Por outro lado, a utilização da expressão yield break é executada de forma diferente. Se a expressão estiver contida num bloco try...finally, as instruções contidas no bloco finally são executadas. Após o fim dessas execuções, o processamento é devolvido ao cliente (método que evocou o bloco iterator). Este método será sempre o MoveNext ou o Dispose.


Blocos iterators internals


Mas então como é que é possível executar as acções descritas anteriormente? A forma mais simples de percebermos o que se passa á construirmos um pequeno programa e analisarmos o código gerado pelo compilador:



namespace TestCode
{
public class Test
{
int [] _arr = { 1, 2, 3 };
public Test( )
{
}
public IEnumerator GetEnumerator( )
{
for ( int i = 0; i < _arr.Length; i++ )
{
yield return _arr [i];
}
}
}
class Program
{
static void Main( string [] args )
{
Test t = new Test( );
foreach ( object a in t )
{
Console.WriteLine( a );
}
Console.Read();
}
}
}


O código é muito simples. Apresenta uma classe que expõe um membro que devolve um enumerator (GetEnumerator) que é utilizado para percorrer os elementos guardados numa hipotética colecção. Mas então qual será o código gerado pelo compilador? Está na altura de recorrermos à minha ferramenta preferida - .Net Reflector - e de averiguarmos o código final:



public class Test
{
// Methods
public Test();
public IEnumerator GetEnumerator();
// Fields
private int[] _arr;
// Nested Types
private sealed class d__0 : IEnumerator<int>, IEnumerator, IDisposable
{
// Methods
public d__0(int <>1__state);
private bool MoveNext();
void IEnumerator.Reset();
void IDisposable.Dispose();
// Properties
int IEnumerator.Current { get; }
object IEnumerator.Current { get; }
// Fields
private int <>1__state;
private int <>2__current;
public Test <>4__this;
public int <i>5__1;
}
}


A primeira observação importante decorre do facto do compilador gerar uma nova classe interna que irá implementar o próprio enumerator. Vamos analisar os métodos da nova class interna gerada. Comecemos pelo MoveNext:



private bool MoveNext()
{
switch (this.<>1__state)
{
case 0:
{
this.<>1__state = -1;
this.<i>5__1 = 0;
goto Label_005D;
}
case 1:
{
goto Label_0048;
}
}
goto Label_0072;
Label_0027:
this.<i>2__current = this.<>4__this._arr[this.5__1];
this.<>1__state = 1;
return true;
Label_0048:
this.<>1__state = -1;
++this.<i>5__1;
Label_005D:
if (this.<i>5__1 < this.<>4__this._arr.Length)
{
goto Label_0027;
}
Label_0072:
return false;
}



Como é possível averiguar, este método é o verdadeiro responsável pela actualização do elemento que irá ser devolvido pelo iterator. A classe interna recorre à variável __state para manter o estado interno do iterator. Para além disso, a variável apresentada com o nome <>4__this mantém uma cópia interna (ou melhor, uma referência) da nossa classe que contém os itens que vão ser percorridos. Repare-se como o compilador transformou o método original GetEnumerator (que recorria aos yields) numa máquina de estado. O estado é controlado pela variável __state (como referimos acima) e as instruções que devem ser executados dependem efectivamente do estado dessa variável. Assim, no início (estado 0) a classe tem de modificar o seu estado interno e tem de actualizar o valor da propriedade Current (se for possível). Nesta altura, é necessário retornar true se obtivémos um elemento, ou false se tal não tiver acontecido (convém notar que, se a colecção tiver vazia, então o estado será actualizado para -1; caso contrário será actualizado para 1).


A próxima evocação do método MoveNext, irá fazer com que o código associado à label Label_0048 seja executado. Isto deve-se ao facto de, nesta segunda evocação, o estado ser igual a 1 (partindo do principio que a primeira evocação do método MoveNext teve sucesso, isto é, devolveu um elemento). Nesta altura, temos de actualizar novamente o estado, incrementar o valor interno que controla a posição e actualizar o valor da propriedade Current. Estes passos são repetidos várias vezes até que a colecção seja percorrida até ao fim.


Já agora, aqui fica o método GetEnumerator:



public IEnumerator GetEnumerator()
{
Test.d__0 d__1 = new Test.d__0(0);
d__1.<>4__this = this;
return d__1;
}



O compilador substituiu o nosso ciclo pela instanciação de uma classe que implementa o enumerator. Para completar os excertos, cá fica o método Main:



private static void Main(string[] args)
{
Test test1 = new Test();
using (IEnumerator enumerator1 = test1.GetEnumerator())
{
while (enumerator1.MoveNext())
{
object obj1 = enumerator1.get_Current();
Console.WriteLine(obj1);
}
}
Console.Read();
}


Uma nota importante: a utilização de blocos iterator não permite fazer o reset ao nosso enumerator. Portanto, se evocarmos o método Reset iremos gerar uma excepção.


Como referi antes, também é possível utilizarmos os blocos iterators quando um método retorna um interface do tipo IEnumerable. Neste caso, o código gerado pelo compilador é diferente. Deixa-se como exercicio a investigação do código gerado nessas situações. Um excerto interessante para perceber o que se passa é o seguinte:



class Program
{
static IEnumerable FromTo( int from, int to )
{
while ( from <= to )
{
yield return from++;
}
}
static void Main( string [] args )
{
IEnumerable e = FromTo( 1, 10 );
foreach ( int a in e )
{
Console.Write( a );
}
}
}

Conclusões finais


Ao longo desta série apresentei as principais novas funcionalidades existentes a nível da linguagem C# 2.0. Hoje abordei os iterators. Esta é uma feature muito interessante e, na minha opinião, muito útil. Parece-me a mim que implementarmos um interface complexo como o IEnumerator com recurso a duas ou três linhas de código é, de facto, algo de fenomenal!


Por favor enviem-me as vossas opiniões/sugestões/críticas/correcções para progC@netmadeira.com.


Fiquem bem e boa programação! Até à próxima.


Leiam o meu blog em: http://weblogs.pontonetpt.com/luisabreu


Written By: labreu
Date Posted: 4/14/2006
Number of Views: 438

Return