C#  -  Iteradores e Lazy Execution


 Hoje vamos recordar o conceito de execução preguiçosa ou Lazy Execution em iteradores ou iterators na linguagem  C#.

Quase todos os programas que você escrever terão alguma necessidade de iterar em uma coleção e examinar cada item da coleção. Para isso podemos criar métodos iteradores que produzem um iterador para os elementos dessa classe. Um iterador é um objeto que atravessa um contêiner, especialmente listas.

 Os iteradores podem ser usados para:

Dessa forma um iterator ou iterador é um método ou propriedade implementado com um bloco iterador, que por sua vez é apenas um bloco de código usando as instruções yield return ou yield break. Os blocos Iterador podem ser usados ​​apenas para implementar métodos ou propriedades com um dos seguintes  tipos de retorno:

Cada iterador tem um tipo yield baseado em seu tipo de retorno. Se o tipo de retorno for uma das interfaces não genéricas, o tipo de yield será um objeto,  caso contrário, é o argumento de tipo fornecido para a interface.

Pontos importantes a destacar sobre iteradores:

Por exemplo, o tipo yield de um método que retorna IEnumerator<string> é uma string.

As instruções yield return fornecem valores para a sequência retornada e a instrução yield break encerrará uma sequência.

No código abaixo temos um método iterator simples :

Abaixo temos o resultado a execução deste código :

Podemos alterar o método para criar um List<int>, substituindo cada instrução yield return por uma chamada para Add() e, em seguida,
retornar a lista no final do método. A saída do loop seria exatamente a mesma, mas não funcionaria da mesma maneira.

A grande diferença é que os iteradores são executado de forma deferida conhecida como execução preguiçosa ou lazy execution.

Execução Preguiçosa ou Lazy Execution

A execução preguiçosa, ou avaliação preguiçosa, foi inventada como parte do cálculo lambda na década de 1930.

A ideia básica disso é simples : execute o código apenas quando precisar do valor que ele vai computar.

Para explicar como o código é executado, a listagem a abaixo expande o loop foreach em um código equivalente que usa um loop while. Usamos também a instrução using que chamará Dispose automaticamente, apenas para simplificar :

Vamos entender este código:

Observe que estamos usando as interfaces IEnumerable/IEnumerator (e seus equivalentes genéricos).

Um IEnumerable é uma sequência que pode ser iterada, enquanto um IEnumerator é como um cursor dentro de uma sequência. Várias instâncias de IEnumerator podem iterar no mesmo IEnumerable sem alterar seu estado de forma alguma.

Compare isso com um IEnumerator, que naturalmente tem estado mutável: cada vez que você chama MoveNext(), você está pedindo para mover o cursor para o próximo elemento da sequência sobre a qual está iterando.

Pense em um IEnumerable como um livro e em um IEnumerator como um marcador.

Pode  haver vários marcadores dentro de um livro a qualquer momento, e, mover um marcador para a próxima página não muda o livro ou qualquer um dos outros marcadores, mas altera o estado desse marcador : sua posição dentro do livro.

O método IEnumerable.GetEnumerator() é uma espécie de bootstrapping ou inicilizador: pede à sequência para criar um IEnumerator configurado para iterar nessa sequência, assim como colocar um novo marcador no início de um livro.

Depois de ter um IEnumerator, você chama MoveNext() repetidamente; se ele retornar true, isso significa que você mudou para outro valor que pode acessar usando a propriedade Current. Se MoveNext() retornar false, você chegou ao final da sequência.

O que isso tem a ver com avaliação preguiçosa?

Quando CreateSimpleIterator() é chamado, nenhum corpo de método é executado. Se você colocar um breakpoint na primeira linha (yield return 10) e percorrer o código, você não atingirá o ponto de interrupção ao chamar o método. Você também não atingirá o ponto de interrupção ao chamar GetEnumerator().

O corpo do método começa a ser executado somente quando MoveNext() for chamado.

Mas o que acontece então ?

Mesmo quando o método começa a ser executado, ele vai apenas até onde precisa. Ele para de ser executado quando ocorre uma das seguintes situações:

Se uma exceção for lançada, essa exceção será propagada normalmente. Se o fim for atingido ou uma instrução yield break for atingida, o método MoveNext() retorna false para indicar que você chegou ao final da sequência. Se você chegar a um yield return, a propriedade Current é definida para o valor que você está produzindo e  MoveNext() retorna true.

Assim que MoveNext() começa a iterar, ele atinge a instrução yield return 10; define Current como 10 e, em seguida, retorna true.

Isso tudo parece simples para a primeira chamada para MoveNext(), mas e as chamadas subsequentes ? 

Você não pode recomeçar do zero; caso contrário, a sequência seria 10 repetido um número infinito de vezes. Em vez disso, quando MoveNext() retorna, é como se o método estivesse pausado.

O código gerado acompanha o ponto que você alcançou no método com qualquer outro estado, como a variável local i em seu loop. Quando MoveNext() é chamado novamente, a execução começa a partir do ponto que você alcançou e continua.

É isso que é a execução preguiçosa.

"Meus filhinhos, escrevo-lhes estas coisas para que vocês não pequem. Se, porém, alguém pecar, temos um intercessor junto ao Pai, Jesus Cristo, o Justo."
1 João 2:1

Referências:


José Carlos Macoratti