Vamos entender o IEnumerable? – Parte 02
Dando continuidade ao seu artigo sobre IEnumerable, neste artigo, Gabriel Schade mostra os efeitos colaterais de usar essa interface de maneira errada.

Olá, pessoal!
Daremos continuidade ao artigo sobre IEnumerable em C#. Dessa vez veremos os efeitos colaterais de usar essa interface de maneira errada.
Caso não faça ideia do que eu estou falando, você pode conferir a primeira parte do artigo sobre o IEnumerable:
A leitura do primeiro artigo não é obrigatória para você entender este, mas recomendo, pra que você possa compreender melhor como o IEnumerable funciona.
Neste artigo veremos as diferenças de comportamento entre um IEnumerable e um List. Parece simples, não?
Começaremos com o seguinte exemplo de código:
IEnumerable<int> colecao = new int[] { 1, 3, 5, 6, 7 };
int[] array = colecao.Where(value => value % 2 == 0)
.Select(value => value * value)
.ToArray();Este é um código bastante simples – você provavelmente já deve ter visto algo parecido.
Mas você saberia me dizer como este código realmente é executado? Quantos elementos passam pelo Where? E pelo Select?
Se o código fosse executado de maneira “tradicional”, como são duas chamadas distintas, o algoritmo percorreria toda a coleção removendo os elementos que não passam pelo filtro e depois disso elevando os elementos que sobraram ao quadrado.
Neste caso, esse algoritmo teria complexidade O(N) + O(M), onde N é o tamanho do array antes do filtro, e M é o tamanho do array depois do filtro.
No entanto, a complexidade real deste algoritmo é de apenas O(N). Isso porque as operações acontecem uma após a outra durante o mesmo loop – o que é bastante interessante, mas pode causar alguns efeitos comportamentais que, se não forem antecipados, podem dar uma boa dor de cabeça.
Vamos começar fazendo o exemplo utilizando uma lista, onde as coisas são executadas de maneira “tradicional”. Para começar, criaremos o método que gera a lista. Esse método fará um loop de zero até nove, adicionando o número na lista:
List<int> GenerateList()
{
List<int> list = new List<int>();
for (int index = 0; index < 10; index++)
{
Console.WriteLine($"The value {index} has been added.");
list.Add(index);
}
return list;
}Colocaremos mensagens no console na interação com cada elemento. Dessa forma, facilitamos a visualização do que está ocorrendo durante a execução.
No método Main faremos algumas operações com essa lista. Primeiro vamos simplesmente fazer um loop utilizando o foreach:
static void Main(string[] args)
{
List<int> list = GenerateList();
Console.WriteLine("");
Console.WriteLine("List has been created");
foreach (var value in list)
{
Console.WriteLine($"The value {value} has been found.");
}
Console.ReadKey();
}Agora continuaremos fazendo mais três operações:
- 1. Mostrar o resultado da propriedade Count
- 2. Mostrar o resultado do método Any
- 3. Executar o método ToArray
//...
Console.WriteLine("");
Console.WriteLine($"Count: {list.Count}");
Console.WriteLine("");
Console.WriteLine($"Any: {list.Any()}");
Console.WriteLine("");
int[] array = list.ToArray();
//...Vamos executar e ver o resultado dessas operações:

Até aqui nenhuma grande surpresa: as coisas são executadas na ordem em que foram declaradas, primeiro quando cada elemento é adicionado na lista, depois no foreach, Count e, por fim, no Any.
Faremos a mesma coisa, mas desta vez vamos trabalhar com um IEnumerable ao invés de uma lista.
Começaremos com o método para gerar o IEnumerable. Dessa vez, usando a palavra reservada yield:
IEnumerable<int> GenerateIEnumerable()
{
for (int index = 0; index < 10; index++)
{
Console.WriteLine($"The value {index} has been added.");
yield return index;
}
}O método em sua essência é o mesmo: gera uma coleção exibindo uma mensagem ao passar por cada elemento.
Todo o corpo do método principal não precisa ser alterado. Basta alterarmos o tipo da lista para um IEnumerable e apontar a chamada para criação da coleção para o novo método:
static void Main(string[] args)
{
IEnumerable<int> ienumerable = GenerateIEnumerable();
//...
}Antes de executar, você consegue prever o resultado? Talvez se assuste um pouco.
Mas vamos lá:

Se você se surpreendeu com o resultado, tenho más notícias. Talvez você esteja piorando seu software sem nem se dar conta disso.
Note que foram feitas muitas operações a mais do que no caso da lista. Isso significa que usar a lista é melhor que usar um IEnumerable? De forma alguma!
Simplesmente estávamos trabalhando com o IEnumerable como se fosse uma lista, o que claramente não é o caso. Vamos a um passo a passo para entender o que houve.
Note que a mensagem notificando que o IEnumerable foi gerado foi exibida antes das mensagens que notificam que cada elemento foi adicionado na coleção. Por que?
Isso ocorre devido ao fato de o IEnumerable possuir uma característica de avaliação lazy, ou seja, o valor de um IEnumerable não está armazenado verdadeiramente na estrutura – ele só será computado quando precisarmos dele.
Esse é a razão dele conseguir realizar as operações no mesmo loop, o que pode causar um imenso ganho de performance (ou perda, quando usado de forma errada).
Note que quando começamos a iterar cada elemento no foreach ele é computado individualmente, ou seja, o nosso yield return é executado de um elemento, e ainda nesta iteração, a operação interna do foreach já é executada sobre o mesmo elemento.
No caso da lista, criar a lista e percorrê-la possuía complexidade O(N²), enquanto no caso do IEnumerable realizamos estas duas operações em O(N). Legal, né?
Depois disso vimos que a lista é gerada novamente quando utilizamos o Count, mas por que diabos isso acontece?
Você sempre precisa lembrar que o IEnumerable não contém os dados de verdade, ele precisa computá-los – então para contar quantos elementos existem na lista ele irá percorrê-la até o fim.
No caso da lista, já sabíamos quantos elementos ela continha. Por conta disso, Count é uma propriedade dentro da lista e um método no IEnumerable. A diferença em termos de execução aqui também é gritante.
No caso da lista temos a complexidade O(1). Afinal, basta checarmos uma propriedade que já contém esse valor armazenado, enquanto no IEnumerable temos a complexidade O(N).
Imagine que o Count do IEnumerable seja um método similar a este:
int Count<T>(IEnumerable<T> source)
{
int count = 0;
using (IEnumerator<T> enumerator = source.GetEnumerator())
{
while (enumerator.MoveNext())
{
count++;
}
}
return count;
}Talvez agora pareça um pouco mais doloroso aquele seu Count() > 0 que eu sei que você já fez. O que nos leva ao próximo método utilizado no exemplo: Any().
Sempre que você precisar verificar: Count() > 0 em um IEnumerable, por favor, substitua-o por um Any(), não é de brincadeira que existem dois métodos.
Se você notar na saída do console, o método Any() exibiu apenas a mensagem do primeiro elemento sendo adicionado. Por que?
Simples, se houver qualquer elemento na coleção, ele já deve retornar true, então não faz sentido percorrer a coleção inteira, o que nos deixa sempre com a complexidade O(1).
O método Any é um método similar ao método abaixo:
bool Any<T>(IEnumerable<T> source)
{
using (IEnumerator<T> enumerator = source.GetEnumerator())
{
return enumerator.MoveNext();
}
}Por fim, ao realizarmos o ToArray(), notamos que o IEnumerable é computado mais uma vez. Ou seja, tome muito cuidado com suas transformações de coleções – você pode estar gerando computação extra por nada.
Mas, afinal, qual a melhor solução?
Neste caso, o correto seria transformarmos o IEnumerable em uma lista ou em um array no momento certo. Ou seja, temos que garantir que os dois loops sejam executados em conjunto, e que as operações seguintes (Count e Any) possam ser executadas em O(1).
Como fazer?
Teremos que realizar alguns ajustes. Isso porque o loop foreach forçará a avaliação do IEnumerable se ele ficar neste escopo. Precisamos mudá-lo para fazer com que ele continue o comportamento lazy do IEnumerable.
Aqui temos dois caminhos distintos:
- 1. Utilizar o Select ao invés de um foreach
- 2. Transformar essa iteração em um método separado
Primeiro faremos a implementação utilizando o método Select. Para isso, faremos a chamada da criação do IEnumerable normalmente, e depois vamos executar um Select seguido de um ToList.
A partir daí salvaremos o valor em uma lista para podermos executar as próximas operações sem percorrer todos os elementos, conforme o código:
static void Main(string[] args)
{
IEnumerable<int> ienumerable = GenerateIEnumerable();
Console.WriteLine("");
Console.WriteLine("IEnumerable has been created");
List<int> list = ienumerable
.Select(value =>
{
Console.WriteLine($"The value {value} has been found.");
return value;
})
.ToList();
Console.WriteLine("");
Console.WriteLine($"Count: {list.Count}");
Console.WriteLine("");
Console.WriteLine($"Any: {list.Any()}");
Console.ReadKey();
}Atenção
- Vale lembrar que isso é apenas para exemplificar. O método Select não deve causar nenhum tipo de efeito colateral, como uma saída no console.
Dessa forma, nossa implementação consegue utilizar o melhor dos dois mundos, usando cada tipo no seu melhor caso.
A segunda opção de implementação é transformar o loop em um método, retornando uma nova coleção através do yield return, conforme o código abaixo:
static IEnumerable<T> FindElements<T>(IEnumerable<T> source)
{
foreach (T value in source)
{
Console.WriteLine($"The value {value} has been found.");
yield return value;
}
}Agora precisamos chamar essa nova função antes de transformar o IEnumerable em uma lista:
static void Main(string[] args)
{
IEnumerable<int> ienumerable = GenerateIEnumerable();
Console.WriteLine("");
Console.WriteLine("IEnumerable has been created");
ienumerable = FindElements(ienumerable);
List<int> list = ienumerable.ToList();
Console.WriteLine("");
Console.WriteLine($"Count: {list.Count}");
Console.WriteLine("");
Console.WriteLine($"Any: {list.Any()}");
Console.ReadKey();
}Com isso, temos o resultado esperado:

Atenção
- Usei a estrutura List para exemplificar, mas sempre que você não precisar alterar o tamanho da coleção, prefira utilizar um array.
A lição principal deste artigo, é: utilize as coisas com sabedoria. Nem sempre o seu ToList() está errado, e às vezes, manter tudo como IEnumerable custa bem caro.
O ideal é entender até onde vale manter a estrutura como um IEnumerable e realizar a computação para uma estrutura em memória quando precisar.
Gostou desse tipo de artigo? Me conte nos comentários.
Até mais!








