Entendendo LINQ #1: Como filtrar itens de uma coleção com Where

Jéferson Bueno
CWI Software
Published in
5 min readFeb 6, 2019

Prólogo

Minha ideia é criar uma pequena série de posts explicando brevemente o que fazem alguns dos métodos de extensão do namespace System.Linq, como usá-los e como eles funcionam por trás das cortinas.

Disclaimer: Nestes posts falo apenas sobre os métodos do chamado LINQ to Objects. Sendo assim, nada do que for dito aqui se aplica aos métodos LINQ de providers específicos (Entity Framework, XML, NHibernate, entre outros).

Uma das operações mais comuns para se fazer em coleções de dados é filtrá-los. Instintivamente, a implementação que vem a cabeça é criar uma nova coleção com base na original, contendo todos os elementos que correspondam a condição do filtro.

Vamos a um breve exemplo. Imagine que precisamos resolver o problema do enunciado a seguir.

Tendo uma lista com todos os números de 1 a 30, escreva um código que mostre na tela apenas os números pares desta lista.

A implementação mais óbvia, seria algo parecido com o código abaixo.

Observação: Usei o método Range para gerar a coleção de números, isso foi para facilitar a escrita do código e focar na parte realmente importante.

Veja funcionando no Repl.it

O método ApenasPares recebe uma coleção de inteiros como entrada, cria uma nova lista para representar a saída, itera sobre cada elemento da entrada e, se o elemento atender a condição item % 2 == 0, ele é adicionado à lista de saída.

E é assim mesmo que se faz?

Sim! A essência é justamente essa: criar uma nova coleção com os dados que atenderem a condição do filtro.

Mas vamos complicar um pouco. E se agora eu te pedisse para retornar todos os números que sejam múltiplos de 3? Bem, continua fácil, é só adaptar o método para receber um parâmetro dizendo qual é o múltiplo que precisa ser checado. Algo como:

List<int> ApenasMultiplosDe(IEnumerable<int> entrada, int m) 
{
var saida = new List<int>();
foreach(var item in entrada)
{
if(item % m == 0)
saida.Add(item);
}
return saida;
}

Sagaz! Mas e se a tarefa agora for retornar todos os números que sejam múltiplos de 3 e 5 ao mesmo tempo e que sejam maiores do que 15? Ou retornar todos os números ímpares?

Qual seria a melhor solução? Criar um método para cada condição diferente? Desistir de filtrar os dados? Pedir uma tarefa mais fácil?

É aí que entra o tal do LINQ e o método Where.

Where e… MAGIC 🧙‍♂️

Veja funcionando no Repl.it

Com o código acima, resolvi todos os requisitos descritos anteriormente usando apenas uma linha de código para cada um.

E como isso funciona?

De forma geral, bem parecido com a implementação de filtro que fizemos acima, no método ApenasPares().

O Where é um método de extensão disponível para qualquer coleção enumerável (que implemente IEnumerable). Este método recebe como parâmetro implícito a coleção que será filtrada e como parâmetro explícito um predicado — função booleana que define a condição que cada item da coleção deve atender para fazer parte da nova coleção. Esta função é um delegate do tipo Func<T, bool> onde T é o tipo de cada um dos elementos da lista (int, no caso do exemplo).

Esta assinatura de Func define uma função que recebe um parâmetro do tipo T e retorna um booleano.

Vamos tentar entender olhando para o código que escrevemos acima.

numeros.Where(n => n % 3 == 0 && n % 5 == 0)

Primeiramente, numeros é o parâmetro implícito do método de extensão, não quero entrar em detalhes sobre isto, mas só pra deixar claro: uma chamada a um método de extensão é apenas um syntactic sugar para fazer uma chamada a um método estático.

O código compilado é, na verdade:

Enumerable.Where(numeros, n => n % 3 == 0 && n % 5 == 0)

Vamos focar na parte que realmente interessa, o predicado:

n => n % 3 == 0 && n % == 0

Isso é uma função anônima (também chamada de expressão lambda), onde n é o parâmetro que esta função recebe e o que vem após o operador => é a o que será retornado por essa expressão, ou seja, a expressão que será usada para validar os itens da coleção.

Observação: Note que ao escrever a função, não é necessário explicitar o tipo do parâmetro e nem usar a keyword return. O parâmetro é desnecessário porque é inferido a partir do tipo genérico na definição da função que, por sua vez, é inferido a partir do tipo genérico da coleção trabalhada. Ou seja, se for uma lista de strings, o predicado recebido será um Func<string, bool>. Logo, o parâmetro da função será uma string; Se for uma lista de objetos do tipo Cliente, o parâmetro será um objeto do tipo do Cliente.

O return não é necessário porque uma expressão lambda retorna implicitamente o que vem depois do operador =>, desde que ele não seja seguido por um bloco delimitado por chaves.

Pra ilustrar, de forma mais intuitiva, a expressão usada acima pode ser escrita de forma mais verbosa, mas mais fácil de ler por quem não esteja acostumado.

Três formas diferentes de escrever a mesma função lambda

Essa expressão recebida por parâmetro será avaliada para cada um dos valores contidos em numeros e os itens que atenderem a condição farão parte da nova coleção.

A tabela abaixo contém uma ilustração da execução desta expressão para os valores contidos na coleção inicial. Os elementos com o valor TRUE na última coluna atendem ao filtro e farão parte da nova coleção.

Tabela verdade representando os resultados da função para cada entrada

Simples, não? Como eu disse anteriormente, bem parecido com a implementação feita no início do post.

Implementando seu próprio método de filtro

Embora eu tenha dito anteriormente que isso é mágica (e eu quase nunca minto), vou mostrar como é possível, e relativamente fácil, implementar seu próprio método para filtrar coleções de qualquer tipo.

Abaixo, implementei o método de extensão MeuWhere que funciona de forma semelhante ao método Where original.

Aproveitei para criar casos de testes aplicando os filtros também numa coleção de objetos do tipo Usuario para mostrar que, mesmo sendo uma implementação simples, ela funciona independente do tipo da coleção.

Veja funcionando no Repl.it

Simples, não?

O código poderia ser ainda menor, usando yield return e removendo a necessidade de criar uma lista.

Note que, embora esta seja uma implementação funcional, usando as mesmas diretrizes básicas da original, o método Where do LINQ é implementado com algumas abstrações e cuidados a mais. Um pequeno spoiler que posso deixar é: uma das diferenças da implementação original é que a coleção com os dados filtrados não é criada na chamada do Where.

Talvez seja um bom assunto para um próximo post =)

--

--

Jéferson Bueno
CWI Software

Desenvolvo umas paradas na CWI Software e coleciono medalhas do Stack Overflow em Português. Eu sou o LINQ! — https://pt.stackoverflow.com/u/18246