yield return yeah!

Publicado por Alexandre Cunha
27/1/2011
Categoria:
Tags: ,

A Microsoft tem introduzido novidades fantásticas na plataforma .NET nos últimos anos, principalmente nas versões 3.X e 4.0. A library de paralelismo (TPL) é uma coisa do outro mundo. Sim, eu sei que a idéia foi clonada do Java e acho ridículo ficar comparando quem copiou de quem, boas idéias precisam ser copiadas e melhoradas (open source hello?). Essa competição entre as linguagens é ótima para os desenvolvedores então deveríamos incentivá-la e não ser contra. Foi excelente o .NET copiar o Java, imagina se eles tivessem copiado o Delphi que desastre? Além disso todos nós sabemos o que acontece quando um produto tem uma posição dominante no mercado como o IE6 tinha há uns anos atrás: a inovação simplesmente pára e não existem palavras para expressar quão péssimo isso é.

Hoje vou falar de uma feature um pouco mais antiga: o keyword yield return, introduzido na versão 2.0 do .NET; que é muito poderoso mas ainda pouco utilizado. O yield return pode ser extremamente útil quando um método precisa retornar uma coleção de objetos na qual deve ser aplicada alguma lógica específica. Vamos então nos aprofundar um pouco na interface IEnumerable e IEnumerable<T>.

Essas são as duas interfaces que todas (sim todas!) as coleções em .NET implementam. Ela representa um objeto que é “enumerável”, ou seja, é uma coleção. A interface IEnumerable<T> possui um método GetEnumerator que retorna um IEnumerator<T> que é um “enumerador” dessa coleção (da mesma forma a interface IEnumerable retorna um IEnumerator). Esse enumerador iterage sobre os elementos dessa coleção basicamente através do método MoveNext e da propriedade Current. Portanto todas as coleções implementam a interface IEnumerable e é isso que permite que sejam utilizadas dentro de um loop for ou foreach.

Suponha que temos um método RetrieveAccounts que retorna objetos do tipo Account que estão armazenados na memória de um objeto AccountStore.

public class AccountStore
{
    private List<Account> cache;

    public IEnumerable<Account> RetrieveAccounts()
    {
        return cache;
    }
}

Obviamente podemos retornar um objeto List<Account> como sendo um IEnumerable<Account> pois List<T> implementa a interface IEnumerable<T>. Até aqui tudo normal. Agora imagine se quiséssemos implementar um método RetrieveActiveAccounts que retorna apenas as contas ativas. Possivelmente seria implementado assim:

public IEnumerable<Account> RetrieveActiveAccounts()
{
    List<Account> list = new List<Account>();
    foreach (Account account in cache)
        if (account.IsActive)
            list.Add(account);
    return list;
}

static void Main()
{
    foreach (Account account in store.RetrieveActiveAccounts())
    {
        //faz alguma coisa com cada objeto Account
    }
}

Essa solução não é nada elegante pois além de construir um novo objeto List ela força duas iterações sobre o cache: uma para selecionar as contas ativas e outra para iterar sobre o resultado. Criar um novo List para cada chamada do método é uma péssima idéia. Se tivermos um cenário onde essa função é chamada por 10 mil clientes, a memória vai pro espaço rapidinho.

Imagine que temos um cache com 100 mil objetos e 99 mil deles ativos. Não me parece uma boa opção criar uma nova lista com 99 mil objetos só para iterar sobre as contas ativas. Claro que um caso simples como esse você pode jogar o teste (se a conta está ativa) no seu loop for que está fora do método (dentro do Main no exemplo) mas se a lógica do método RetrieveActiveAccounts não for tão simples essa opção vira um problema pois toda vez que você quiser iterar sobre as contas ativas precisa fazer o famoso copy & paste (um daqueles anti patterns que faz o Martin Fowler ter dor de barriga por 1 mês!) .

Uma solução mais elegante para este problema seria implementar um IEnumerable<Account> que fizesse o trabalho de filtrar as contas ativas e portanto não precisaríamos criar uma nova lista toda chamada de método. Vemos a implementação a seguir:

public class ActiveAccountEnumerable : IEnumerable<Account>
{
    private List<Account> list;

    public ActiveAccountEnumerable(List<Account> list)
    {
        this.list = list;
    }

    public IEnumerator<Account> GetEnumerator()
    {
        return new ActiveEnumerator(list);
    }

    private class ActiveEnumerator : IEnumerator<Account>
    {
        private List<Account> list;
        private int pos = -1;

        public ActiveEnumerator(List<Account> list)
        {
            this.list = list;
        }

        public Account Current
        {
            get
            {
                if (pos < 0)
                    throw new InvalidOperationException();
                return list[pos];
            }
        }

        object IEnumerator.Current
        {
            get { return Current; }
        }

        public bool MoveNext()
        {
            do
            {
                pos++;
            } while (pos < list.Count && !list[pos].IsActive);
            if (pos == list.Count)
                return false;
            return true;
        }

        public void Reset()
        {
            pos = -1;
        }

        public void Dispose()
        {
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

Vamos esquecer o fato que eu usei um do-while!! Parece um programa em Pascal, daqueles que eu fazia na faculdade varando madrugada tentando entender pra que diabos servia o chapéu (^) antes da variável (era feliz e não sabia). Eu disse vamos esquecer!

Uma vez implementado o ActiveAccountEnumerable podemos escrever o seguinte código:

public IEnumerable<Account> RetrieveActiveAccounts()
{
    return new ActiveAccountEnumerable(cache);
}

O fato de instanciar um ActiveAccountEnumerable para cada chamada do método é insignificante. O consumo de memória e processamento é praticamente desprezível ainda mais se comparado ao custo de se criar uma lista nova onde são adicionados 99 mil objetos. Agora sim obtivemos uma solução elegante certo? Mas a que custo eu pergunto?

Esse é o tipo de solução “elegante” que os OO-zistas de plantão adoram mas que você olha e ainda assim sente que alguma coisa fede. Você usou duas classes e duas páginas de código (no meu monitor podre de 1600×900) só para resolver um probleminha que um mero if resolveria??? Você deve estar de sacanagem! Vou falar a verdade: It’s all crap! Every last line of it!

O Anders Hejlsberg devia ter pesadelos com o Iterator do Java e no .NET 2.0 ele resolveu dar uma pitada de Python no .NET: nasceu então o yield return. Código por favor!

public IEnumerable<Account> RetrieveActiveAccounts()
{
    foreach (Account account in cache)
        if (account.IsActive)
            yield return account;
}

Simple assim (0rly?)! Todo código de implementação das interfaces IEnumerable<T> e IEnumerator<T> podem ser trocados por 3 linhas de código e um mero yield return. Normalmente não durmo bem quando leio as definições do MSDN mas cabe aqui uma exceção para a definição do yield return:

The yield keyword signals to the compiler that the method in which it appears is an iterator block. The compiler generates a class to implement the behavior that is expressed in the iterator block. In the iterator block, the yield keyword is used together with the return keyword to provide a value to the enumerator object. This is the value that is returned, for example, in each loop of a foreach statement.

Levemos o conceito adiante e vejamos quão interessante ele fica. Vamos supor que o código cliente que chama o método RetrieveAccounts irá mostrar as informações de cada conta na interface e temos uma limitação de que apenas 25 registros podem ser exibidos por vez. Precisamos de um sistema de paginação, vamos experimentar assim:

public IEnumerable<IEnumerable<Account>> RetrieveAccouts()
{
    List<Account> page = new List<Account>();
    foreach (Account account in cache)
    {
        page.Add(account);
        if (page.Count == 25)
        {
            yield return page;
            page.Clear();
        }
    }
    if (page.Count > 0)
        yield return page;
}

Um minuto de silêncio…

É uma solução simples, elegante, fácil de entender e não requer a implementação de quinhentas interfaces que no fim só burocratizam o seu código. O tipo de solução que eu adoro pois ataca o problema onde ele precisa ser atacado e não fica rodeando com OO-zices. Não me entendam mal, eu sou total pro-OO, mas tem horas que realmente atrapalha.

Agora o código cliente para percorrer as contas ativas fica fantástico:

static void Main()
{
    foreach (IEnumerable<Account> page in store.RetrieveActiveAccounts())
    {
        foreach (Account account in page)
        {
            //mostra os objetos na view/UI
        }
        if (IsQueryCancelled())
            break;
    }
}

Sem palavras! Agora não só é possível mostrar o resultado na interface sem precisar esperar que os 99 mil objetos sejam processados como também é possível cancelar a “query” caso o usuário não queira esperar. Imagine que o método RetrieveActiveAccounts esteja buscando 100 mil objetos pela rede; neste caso é altamente recomendável mostrar algum resultado para o usuário e deixar ele decidir se a query deve continuar ou não. Com um sistema muito simples de paginação usando yield return é possível montar esta solução no .NET.

Como eu disse, a competição entre as linguagens sempre traz benefícios aos desenvolvedores. A vontade de inovar e tornar trechos de código complexos em coisas triviais é muito importante para um programador pois ajuda na sua capacidade de abstração uma vez que aquilo deixa de ser um problema indigesto que requer muitos neurônios e passa a ser uma coisa trivial, permitindo que o desenvolvedor se concentre em outras atividades mais importantes. O paradigma OO surgiu justamente para se diminuir a complexidade e melhorar a qualidade do código, mas existem momentos que uma solução simples, trivial e inteligível vale mais que mil objetos.

2 Comentários

  • Reply

    Por Eduardo de Britto Castro em 15 de February de 2011 às 18:17

    Muito bom mesmo! É interessante fazer um debug durante a iteração de fora e ver como se comporta a chamada ao método RetrieveActiveAccounts com e sem yield.
    Usar linq também funciona da mesma forma que a versão com yield:

    return from x in cache where x.IsActive select x;

    • Reply

      Por Alexandre Cunha em 15 de February de 2011 às 20:11

      Sim, com certeza. Eu ia colocar a opção com LINQ no artigo mas achei que ia acabar tirando a atenção do yield return, que era o assunto principal. Vou fazer um outro post só para falar de LINQ e Method Extension!
      Valeu, abs!





Desenvolvido por hacklab/ com WordPress