LINQ: Consultando Dados com Elegância

LINQ: Consultando Dados com Elegância

A mudança de mentalidade que o LINQ traz: descrever o resultado desejado em vez de escrever o passo a passo do percurso. A aula cobre os operadores essenciais para filtrar, ordenar e transformar, o uso sobre objetos próprios, e por que a consulta só executa quando o resultado é percorrido.
Linguagem C#

• • 11 min de leitura

Na aula anterior, entendemos que todas as coleções compartilham a capacidade de serem percorridas, formalizada pelo contrato IEnumerable. Agora colhemos o fruto mais saboroso dessa fundação: o LINQ (sigla, em inglês, para "consulta integrada à linguagem", que se pronuncia "linc"). O LINQ é uma das ferramentas mais elegantes e queridas do C#, e sua ideia é transformadora. Até aqui, para extrair informação de uma coleção — filtrar os itens que interessam, transformá-los, ordená-los —, você escrevia laços manuais, dizendo como percorrer e testar cada elemento. Com o LINQ, você passa a descrever o que quer, de forma declarativa, e a ferramenta produz o resultado. É a diferença entre dar as instruções passo a passo e simplesmente enunciar o objetivo. Nesta aula, você conhecerá essa forma expressiva de trabalhar com dados, que mudará seu dia a dia de programador.

A mudança de mentalidade: descrever, não instruir

Comecemos sentindo a diferença que o LINQ traz, com um problema concreto. Temos uma lista de números e queremos apenas os pares, cada um dobrado, em ordem crescente. Com o que você sabe até agora, escreveria um conjunto de laços e passos:

List<int> numeros = new() { 5, 2, 8, 1, 4, 7, 6 };

// Forma imperativa: você diz COMO fazer, passo a passo.
List<int> resultado = new();
foreach (int n in numeros)
{
    if (n % 2 == 0)              // filtra os pares
        resultado.Add(n * 2);   // adiciona o dobro
}
resultado.Sort();               // ordena

Isso funciona, mas repare: a intenção ("os pares, dobrados, ordenados") fica soterrada sob a mecânica (o laço, o if, a lista intermediária, a ordenação). Agora veja a mesma tarefa com LINQ:

using System.Linq;   // onde o LINQ vive (normalmente já vem incluído)

// Forma declarativa: você diz O QUE quer.
var resultado = numeros
    .Where(n => n % 2 == 0)      // apenas os pares
    .Select(n => n * 2)          // dobrados
    .OrderBy(n => n);            // em ordem crescente

Leia a versão LINQ quase como uma frase: "dos números, onde forem pares, selecione o dobro, ordene crescentemente". A consulta é a descrição do que se deseja. Não há laço visível, não há lista intermediária explícita, não há passos de ordenação separados — apenas a intenção, expressa diretamente. Essa é a mudança de mentalidade do LINQ: você deixa de dizer como percorrer e testar, e passa a dizer o que quer obter. O "como" fica por conta da ferramenta.

Uma primeira olhada na sintaxe estranha

Você deve estar intrigado com aquele n => n % 2 == 0 dentro do Where. Trata-se de uma pequena função anônima, chamada lambda, e ela é o coração de como o LINQ recebe seus critérios. Por ora, peço que a leia intuitivamente, sem se preocupar com a mecânica completa — que as duas próximas aulas revelarão em detalhe. A lambda n => n % 2 == 0 significa, aproximadamente: "para cada elemento, que chamarei de n, o critério é n % 2 == 0". O símbolo => (que já vimos no switch, na Aula #12) separa o "nome dado a cada elemento" (à esquerda) do "que fazer ou testar com ele" (à direita). Assim, Where(n => n % 2 == 0) lê-se "mantenha os elementos onde n é par", e Select(n => n * 2) lê-se "para cada elemento n, produza n * 2". Aceite essa leitura por enquanto; nas próximas aulas você entenderá exatamente o que é uma lambda e por que ela funciona. Aqui, o foco é usar o LINQ, e a leitura intuitiva basta.

Os operadores essenciais

O LINQ oferece muitos "operadores" — métodos que você encadeia para transformar uma coleção —, mas um punhado deles resolve a maioria dos casos. Conheçamos os mais importantes, todos usados com o ponto, sobre qualquer coleção.

O Where filtra: mantém apenas os elementos que satisfazem um critério.

var maioresQue3 = numeros.Where(n => n > 3);   // só os elementos maiores que 3

O Select transforma: produz um novo valor a partir de cada elemento.

var quadrados = numeros.Select(n => n * n);    // cada número ao quadrado

O OrderBy e o OrderByDescending ordenam, crescente e decrescentemente:

var crescente = numeros.OrderBy(n => n);
var decrescente = numeros.OrderByDescending(n => n);

E há operadores que produzem uma resposta única a partir da coleção — o que chamamos de agregações: Count (quantos), Sum (soma), Average (média), Max e Min (maior e menor):

Console.WriteLine(numeros.Count(n => n % 2 == 0));   // quantos são pares
Console.WriteLine(numeros.Sum());                     // a soma de todos
Console.WriteLine(numeros.Max());                     // o maior valor

Repare que esses operadores substituem, com uma única chamada expressiva, os padrões de processamento que aprendemos manualmente na Aula #21: o Count(...) faz a contagem condicional, o Sum() faz a acumulação, o Max() faz o "maior valor". Onde antes escrevíamos um laço com um acumulador, agora escrevemos uma palavra. O LINQ não faz nada que você não pudesse fazer com laços — ele apenas o expressa de forma muito mais concisa e legível.

O LINQ brilha sobre objetos

O verdadeiro poder do LINQ aparece quando trabalhamos com coleções de objetos — as classes que você aprendeu a criar. Suponha uma lista de produtos, e queremos os nomes dos produtos de certa categoria com preço abaixo de um limite, em ordem alfabética:

class Produto
{
    public string Nome { get; set; }
    public decimal Preco { get; set; }
    public string Categoria { get; set; }
}

List<Produto> produtos = new()
{
    new Produto { Nome = "Suco", Preco = 8m, Categoria = "Bebida" },
    new Produto { Nome = "Refrigerante", Preco = 6m, Categoria = "Bebida" },
    new Produto { Nome = "Água", Preco = 3m, Categoria = "Bebida" },
    new Produto { Nome = "Pão", Preco = 5m, Categoria = "Padaria" }
};

// "Os nomes das bebidas abaixo de R$ 10, em ordem alfabética."
var bebidas = produtos
    .Where(p => p.Categoria == "Bebida" && p.Preco < 10)
    .OrderBy(p => p.Nome)
    .Select(p => p.Nome);

foreach (var nome in bebidas)
    Console.WriteLine(nome);   // Água, Refrigerante, Suco

A consulta lê-se quase como a frase em português que a descreve: filtra as bebidas com preço abaixo de 10, ordena por nome, e seleciona os nomes. Dentro das lambdas, acessamos as properties dos objetos (p.Categoria, p.Preco, p.Nome) normalmente. Compare o esforço mental de escrever essa consulta com o de escrever os laços equivalentes: o LINQ reduz drasticamente a distância entre o que você pensa e o que você escreve. Essa proximidade entre a intenção e o código é a grande dádiva do LINQ, e é o que o torna tão querido.

Materializando o resultado

Uma observação prática importante, ligada à ideia de "entrega sob demanda" que plantamos na aula anterior. Uma consulta LINQ, quando escrita, ainda não executa de imediato — ela apenas descreve o que fazer. A execução acontece quando você consome o resultado, tipicamente ao percorrê-lo com foreach, ou ao pedir explicitamente que ele seja transformado numa lista concreta com o método ToList():

List<int> paresDobrados = numeros
    .Where(n => n % 2 == 0)
    .Select(n => n * 2)
    .ToList();   // AQUI a consulta executa e produz uma List concreta

Na prática, a regra é simples: encadeie os operadores à vontade para descrever o que quer (é barato, pois nada roda ainda), e chame ToList() quando precisar do resultado como uma lista concreta que você vai guardar, percorrer mais de uma vez, ou passar adiante. Se você apenas vai percorrer o resultado uma vez com foreach, nem precisa do ToList() — o foreach já dispara a execução. Não se preocupe em dominar todas as sutilezas dessa "execução adiada" agora; guarde a regra prática do ToList(), e ela o servirá bem.

A forma de trabalhar com dados mudou de natureza. Em vez de escrever o passo a passo do percurso, passa-se a descrever o resultado desejado — filtrar, ordenar, transformar, agrupar — e deixar que a linguagem cuide da execução. Um bloco de laços e condições encolhe para uma expressão que se lê quase como uma frase.

Os operadores essenciais já cobrem a maior parte do uso diário, e a mecânica da materialização ficou registrada como o detalhe que evita surpresas: a consulta não roda no instante em que é escrita, e sim quando seu resultado é efetivamente percorrido ou convertido numa coleção. Sobre objetos próprios, essa expressividade é o que aproxima o código da linguagem do problema.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Dada List<int> n = new() { 5, 2, 8, 1, 4, 7, 6 };, escreva uma consulta LINQ que retorne os números maiores que 3, cada um ao quadrado, em ordem crescente. Percorra o resultado com foreach e exiba-o. Depois, escreva a versão equivalente com laço manual e compare a legibilidade.

Ver resposta

✓ Resposta: Consulta:

var r = n.Where(x => x > 3).Select(x => x * x).OrderBy(x => x);
foreach (var v in r) Console.WriteLine(v);

Laço manual:

var r = new List<int>();
foreach (var x in n) if (x > 3) r.Add(x * x);
r.Sort();

As duas produzem o mesmo resultado, mas a versão LINQ expressa a intenção ("maiores que 3, ao quadrado, ordenados") numa linha declarativa, sem lista intermediária explícita nem passo de ordenação separado, enquanto a imperativa mistura filtro, transformação e ordenação em mecânica manual mais verbosa.

Exercício 2

Usando os operadores de agregação, escreva expressões LINQ que calculem, para a lista acima: a soma de todos os números; quantos são pares; e o maior número. Relacione cada uma com um dos "padrões de processamento" que você aprendeu manualmente na Aula #21.

Ver resposta

✓ Resposta: Exemplos:

Console.WriteLine(n.Sum());                    // soma → padrão de acumulação
Console.WriteLine(n.Count(x => x % 2 == 0));   // pares → padrão de contagem condicional
Console.WriteLine(n.Max());                     // maior → padrão de maior/menor

Cada agregação LINQ corresponde a um padrão da Aula #21: o Sum() substitui o laço acumulador da acumulação; o Count(x => ...) substitui o laço com if e contador da contagem condicional; o Max() substitui o laço que rastreia o maior valor. O LINQ expressa em uma palavra o que antes era um laço inteiro.

Exercício 3

Crie uma List<Produto> (com properties Nome, Preco, Categoria) com alguns itens, e escreva uma consulta LINQ que retorne os nomes dos produtos da categoria "Padaria" com preço abaixo de 10, em ordem alfabética. Exiba o resultado.

Ver resposta

✓ Resposta: Exemplo:

List<Produto> produtos = new()
{
    new Produto { Nome = "Pão", Preco = 5m, Categoria = "Padaria" },
    new Produto { Nome = "Bolo", Preco = 25m, Categoria = "Padaria" },
    new Produto { Nome = "Biscoito", Preco = 4m, Categoria = "Padaria" }
};
var itens = produtos
    .Where(p => p.Categoria == "Padaria" && p.Preco < 10)
    .OrderBy(p => p.Nome)
    .Select(p => p.Nome);
foreach (var nome in itens) Console.WriteLine(nome);   // Biscoito, Pão

Exercício 4

Explique, com suas palavras, a diferença entre a abordagem imperativa (laços manuais) e a declarativa (LINQ) para processar coleções. O que significa dizer que, com o LINQ, "você descreve o que quer, não como fazer"?

Ver resposta

✓ Resposta: A abordagem imperativa (laços manuais) consiste em escrever, passo a passo, como percorrer a coleção e o que fazer com cada elemento — o laço, a condição, a coleção intermediária, a ordenação, tudo explícito. A abordagem declarativa (LINQ) consiste em descrever o resultado desejado — quais elementos manter, como transformá-los, como ordená-los — deixando a mecânica de percorrer por conta da ferramenta. Dizer que "com o LINQ você descreve o que quer, não como fazer" significa que você enuncia o objetivo (por exemplo, "os pares, dobrados, ordenados") em vez de detalhar os passos para alcançá-lo; a intenção fica visível no código, e os detalhes de execução ficam ocultos, tornando o código mais legível e mais próximo do raciocínio humano.

Exercício 5

Sem código: a aula afirma que o LINQ "não faz nada que você não pudesse fazer com laços". Se é assim, qual é, então, o valor do LINQ? Por que vale a pena aprendê-lo, se os laços já resolviam os mesmos problemas?

Ver resposta

✓ Resposta: O valor do LINQ não está em fazer algo impossível com laços — de fato, tudo que ele faz poderia ser feito manualmente —, mas em como ele expressa esse processamento: de forma muito mais concisa, legível e próxima da intenção. Vale a pena aprendê-lo porque a legibilidade é um dos maiores valores na programação: uma consulta LINQ de três linhas comunica seu propósito imediatamente ("filtra, transforma, ordena"), enquanto o laço equivalente exige que quem lê reconstrua a intenção a partir da mecânica. Além disso, o LINQ reduz a chance de erros (não há índices para errar, nem listas intermediárias para gerenciar) e torna o código mais fácil de modificar. Em resumo, o LINQ não amplia o que você pode fazer, mas melhora enormemente como você o faz — e código mais claro e menos propenso a erros é um ganho que justifica plenamente o aprendizado.

Comentários

Mais em Linguagem C#

Genéricos: Escreva uma Vez, Use com Qualquer Tipo
Genéricos: Escreva uma Vez, Use com Qualquer Tipo

Como escrever uma estrutura ou algoritmo uma única vez e usá-lo com qualquer…

Encerramento: Você é um Programador
Encerramento: Você é um Programador

O encerramento do núcleo do curso: uma retrospectiva da transformação vivida…

Conversando com o Usuário: Entrada e Saída
Conversando com o Usuário: Entrada e Saída

Como fazer o programa conversar com quem o usa: exibir informação com…