C# O que é LINQ

LINQ (pronuncia-se "link") quer dizer Language Integrated Query, "consulta integrada à linguagem". É o jeito do C# de fazer perguntas a uma coleção: "quais produtos custam menos de R$ 20?", "em ordem de preço", "só os nomes". Em vez de escrever o laço que procura, separa e ordena, você descreve o que quer, e o LINQ faz o trabalho. Esta aula mostra a ideia, as duas formas de escrever e o detalhe mais importante de todos: a consulta só roda quando você pede o resultado.

Você vai aprender
  • O que é uma consulta LINQ, comparada com um laço.
  • A sintaxe de método: produtos.Where(…).OrderBy(…).Select(…).
  • A sintaxe de consulta: from p in produtos where … orderby … select ….
  • O que é o IEnumerable<T> que o LINQ devolve.
  • A execução adiada: nada roda até você percorrer o resultado (com demonstração).
  • Quando usar ToList() para "fotografar" o resultado.
  • Os erros reais: CS0742, CS1936 e a InvalidOperationException de mudar a lista no meio do caminho.

Todos os programas rodaram com o .NET 10 (C# 14) na máquina da Ana, na pasta ~/cs, com dotnet run arquivo.cs. O LINQ mora no namespace System.Linq, que já vem importado nos programas do .NET 6 em diante (em código antigo, você vai ver using System.Linq; no topo). Para entender esta aula, você precisa de Lambdas, de Func e, de preferência, de Métodos de Extensão.


A ideia: pedir em vez de procurar

O estoque da lojinha da Ana está numa lista de produtos. Para saber "quais itens de papelaria custam menos de R$ 20, do mais barato ao mais caro", ela tem dois caminhos:

  • Procurar ela mesma: passar prateleira por prateleira, anotar num papel os que servem, depois reescrever o papel em ordem de preço.
  • Pedir ao Bruno, o estoquista: "me traga os de papelaria abaixo de R$ 20, em ordem de preço, só os nomes". Ela diz o quê; ele sabe como.

O laço é a Ana procurando. O LINQ é o pedido ao Bruno. Todos os exemplos desta aula usam a mesma lista:

List<Produto> produtos =
[
    new("Caderno", "Papelaria", 18.90m, 12),
    new("Lápis", "Papelaria", 2.50m, 40),
    new("Mochila", "Bolsas", 129.90m, 3),
    new("Borracha", "Papelaria", 1.75m, 25),
    new("Estojo", "Bolsas", 24.90m, 0),
    new("Garrafa", "Cozinha", 35.00m, 8),
    new("Caneca", "Cozinha", 22.50m, 5),
];

// … o programa de cada exemplo …

record Produto(string Nome, string Categoria, decimal Preco, int Estoque);

Cada produto tem nome, categoria, preço (em decimal, como todo dinheiro) e quantas unidades há no estoque. O record da última linha cria o tipo Produto (veja Records).


1. Laço × LINQ

// (a lista de produtos de cima)

// 1. com laço
List<Produto> achados = [];
foreach (Produto p in produtos)
{
    if (p.Categoria == "Papelaria" && p.Preco < 20m)
    {
        achados.Add(p);
    }
}
achados.Sort((a, b) => a.Preco.CompareTo(b.Preco));
foreach (Produto p in achados)
{
    Console.WriteLine(p.Nome);
}

Console.WriteLine("---");

// 2. com LINQ
var nomes = produtos
    .Where(p => p.Categoria == "Papelaria" && p.Preco < 20m)
    .OrderBy(p => p.Preco)
    .Select(p => p.Nome);

foreach (string nome in nomes)
{
    Console.WriteLine(nome);
}

// (o record Produto)
$ dotnet run laco-linq.cs
Borracha
Lápis
Caderno
---
Borracha
Lápis
Caderno

O mesmo resultado. No laço, você lê como o trabalho é feito (cria uma lista, testa, adiciona, ordena) e precisa deduzir o que ele faz. No LINQ, cada linha diz o que faz:

LinhaEm portuguêsAula
produtos"Dos produtos…" (a fonte)
.Where(p => p.Categoria == "Papelaria" && p.Preco < 20m)"…pegue os de papelaria com preço abaixo de 20…" (where = "onde")Where e Select
.OrderBy(p => p.Preco)"…em ordem de preço…" (order by = "ordenar por")OrderBy
.Select(p => p.Nome)"…e me dê só os nomes." (select = "selecionar")Where e Select

Cada método recebe uma lambda que diz o critério: o Where recebe "dado um produto, ele serve?" (um Func<Produto, bool>); o OrderBy recebe "dado um produto, ordene por quê?"; o Select recebe "dado um produto, o que entra no resultado?". E cada um devolve uma sequência nova, que vai para o método seguinte, como numa linha de montagem:

produtos ──► Where(papelaria e < 20) ──► OrderBy(preço) ──► Select(nome) ──► "Borracha", "Lápis", "Caderno"
 7 produtos      sobram 3                  os mesmos 3,        3 textos
                                           em outra ordem

A lista produtos não muda: o LINQ só lê a fonte e produz um resultado novo.

Missão: troque o laço com if por uma consulta LINQ com .Where (categoria "Cozinha") e .Select (só o nome), e mostre o resultado com um foreach. A saída continua Garrafa e Caneca.

List<Produto> produtos =
[
    new("Caderno", "Papelaria", 18.90m, 12),
    new("Lápis", "Papelaria", 2.50m, 40),
    new("Mochila", "Bolsas", 129.90m, 3),
    new("Borracha", "Papelaria", 1.75m, 25),
    new("Estojo", "Bolsas", 24.90m, 0),
    new("Garrafa", "Cozinha", 35.00m, 8),
    new("Caneca", "Cozinha", 22.50m, 5),
];

foreach (Produto p in produtos)
{
    if (p.Categoria == "Cozinha")
    {
        Console.WriteLine(p.Nome);
    }
}

record Produto(string Nome, string Categoria, decimal Preco, int Estoque);
List<Produto> produtos =
[
    new("Caderno", "Papelaria", 18.90m, 12),
    new("Lápis", "Papelaria", 2.50m, 40),
    new("Mochila", "Bolsas", 129.90m, 3),
    new("Borracha", "Papelaria", 1.75m, 25),
    new("Estojo", "Bolsas", 24.90m, 0),
    new("Garrafa", "Cozinha", 35.00m, 8),
    new("Caneca", "Cozinha", 22.50m, 5),
];

var cozinha = produtos
    .Where(p => p.Categoria == "Cozinha")
    .Select(p => p.Nome);

foreach (string nome in cozinha)
{
    Console.WriteLine(nome);
}

record Produto(string Nome, string Categoria, decimal Preco, int Estoque);

2. Duas formas de escrever: método e consulta

O C# aceita a mesma consulta escrita de dois jeitos. A sintaxe de método é a que você acabou de ver. A sintaxe de consulta (query syntax) parece a linguagem SQL dos bancos de dados:

// (a lista de produtos)

var metodo = produtos
    .Where(p => p.Preco < 30m)
    .OrderBy(p => p.Nome)
    .Select(p => $"{p.Nome}: {p.Preco:C}");

var consulta =
    from p in produtos
    where p.Preco < 30m
    orderby p.Nome
    select $"{p.Nome}: {p.Preco:C}";

Console.WriteLine(string.Join(" | ", metodo));
Console.WriteLine(string.Join(" | ", consulta));

// (o record Produto)
$ dotnet run consulta.cs
Borracha: R$ 1,75 | Caderno: R$ 18,90 | Caneca: R$ 22,50 | Estojo: R$ 24,90 | Lápis: R$ 2,50
Borracha: R$ 1,75 | Caderno: R$ 18,90 | Caneca: R$ 22,50 | Estojo: R$ 24,90 | Lápis: R$ 2,50

As duas dão exatamente o mesmo resultado, porque são a mesma coisa: o compilador traduz a sintaxe de consulta para as chamadas de método. Peça por peça:

from p in produtos
"Para cada p em produtos": a fonte, e o nome de cada elemento (é o p das lambdas). Toda consulta começa com from.
where p.Preco < 30m
O filtro. Vira .Where(p => p.Preco < 30m).
orderby p.Nome
A ordem. Vira .OrderBy(p => p.Nome).
select …
O que entra no resultado. Vira .Select(p => …). Toda consulta termina com select ou group (sem isso, CS0742).

A lista acima saiu em ordem alfabética (orderby p.Nome): Borracha, Caderno, Caneca, Estojo, Lápis. A Mochila e a Garrafa ficaram de fora pelo preço.

Sintaxe de consultaSintaxe de método
from p in produtosprodutos
where condição.Where(p => condição)
orderby p.Preco.OrderBy(p => p.Preco)
orderby p.Preco descending.OrderByDescending(p => p.Preco)
orderby p.Categoria, p.Nome.OrderBy(p => p.Categoria).ThenBy(p => p.Nome)
select p.Nome.Select(p => p.Nome)
group p by p.Categoria.GroupBy(p => p.Categoria)
(não existe).Count(), .Sum(…), .First(), .ToList(), .Take(3)…
Qual usar

A sintaxe de método é a mais usada no dia a dia e é a que este guia usa daqui em diante: ela tem todos os métodos (a de consulta não tem Count, Sum, Take…) e combina com as lambdas que você já conhece. A sintaxe de consulta fica mais legível em consultas com vários from ou com junções. Você pode até misturar: (from p in produtos where p.Preco < 30m select p).Count().


3. O que o LINQ devolve: IEnumerable<T>

Até agora usamos var. Com os tipos escritos, fica claro o que cada passo produz:

// (a lista de produtos)

IEnumerable<Produto> comEstoque = produtos.Where(p => p.Estoque > 0);
IEnumerable<string> nomes = comEstoque.Select(p => p.Nome);
List<string> lista = nomes.ToList();
int quantos = nomes.Count();

Console.WriteLine($"{quantos} produtos com estoque: {string.Join(", ", lista)}");

// (o record Produto)
$ dotnet run tipos.cs
6 produtos com estoque: Caderno, Lápis, Mochila, Borracha, Garrafa, Caneca
  • Where e Select devolvem um IEnumerable<T> ("enumerável de T"): uma sequência que dá para percorrer com foreach. O Where devolve produtos; o Select(p => p.Nome) transforma em textos, por isso IEnumerable<string>.
  • ToList() percorre a sequência e guarda tudo numa List<T> de verdade.
  • Count() percorre e devolve um número.

Um IEnumerable<T> que saiu do LINQ não é uma lista pronta: é uma receita de como produzir os elementos. Essa diferença é o assunto da próxima seção, e é a coisa mais importante desta aula.


4. A execução adiada: a consulta só roda quando você pede

Quando você escreve var baratos = produtos.Where(p => p.Preco < 20m);, nenhum produto é testado. O LINQ só guarda a receita: "da lista produtos, os que custam menos de 20". Os produtos só são testados quando alguém percorre o resultado: um foreach, um ToList(), um Count(), um string.Join. Isso se chama execução adiada (deferred execution).

É como o pedido ao Bruno escrito num papel: enquanto ninguém entrega o papel, ninguém vai à prateleira. E quando ele for, vai olhar a prateleira como ela estiver naquela hora.

Demonstração 1: a consulta vê o que mudou depois

List<Produto> produtos =
[
    new("Caderno", "Papelaria", 18.90m, 12),
    new("Lápis", "Papelaria", 2.50m, 40),
];

var baratos = produtos.Where(p => p.Preco < 20m);
List<Produto> fotografia = produtos.Where(p => p.Preco < 20m).ToList();

produtos.Add(new("Borracha", "Papelaria", 1.75m, 25));

Console.WriteLine($"Consulta: {string.Join(", ", baratos.Select(p => p.Nome))}");
Console.WriteLine($"ToList:   {string.Join(", ", fotografia.Select(p => p.Nome))}");

record Produto(string Nome, string Categoria, decimal Preco, int Estoque);
$ dotnet run adiada.cs
Consulta: Caderno, Lápis, Borracha   « a Borracha foi cadastrada DEPOIS da consulta, e apareceu
ToList:   Caderno, Lápis             « o ToList rodou na hora: é uma fotografia
  • baratos é só a receita. Ela rodou na linha do string.Join, quando a Borracha já estava na lista.
  • fotografia rodou na hora, por causa do ToList(), quando a lista tinha só dois produtos. Ela é uma List nova, que não acompanha mais a original.

Demonstração 2: quem roda e quando

Para ver o LINQ trabalhando, as lambdas abaixo escrevem uma linha cada vez que rodam:

// (a lista de produtos)

Console.WriteLine("1) montando a consulta");
var consulta = produtos
    .Where(p =>
    {
        Console.WriteLine($"   Where testa {p.Nome}");
        return p.Preco < 20m;
    })
    .Select(p =>
    {
        Console.WriteLine($"   Select pega {p.Nome}");
        return p.Nome.ToUpper();
    });
Console.WriteLine("2) consulta montada, nada rodou ainda");

Console.WriteLine("3) foreach:");
foreach (string nome in consulta)
{
    Console.WriteLine($"   recebi {nome}");
}

// (o record Produto)
$ dotnet run passo.cs
1) montando a consulta
2) consulta montada, nada rodou ainda
3) foreach:
   Where testa Caderno
   Select pega Caderno
   recebi CADERNO
   Where testa Lápis
   Select pega Lápis
   recebi LÁPIS
   Where testa Mochila         « não passou no Where: o Select nem vê a Mochila
   Where testa Borracha
   Select pega Borracha
   recebi BORRACHA
   Where testa Estojo
   Where testa Garrafa
   Where testa Caneca

Duas lições:

  1. Entre o "1)" e o "2)", nada rodou: montar a consulta não testa produto nenhum.
  2. Os produtos passam pela linha de montagem um de cada vez: o Caderno vai do Where ao Select e chega ao foreach antes de o Lápis ser testado. O LINQ não cria uma lista intermediária com "os que passaram no Where".

Demonstração 3: percorrer duas vezes = rodar duas vezes

// (a lista de produtos)

int testes = 0;
var baratos = produtos.Where(p => { testes++; return p.Preco < 20m; });

Console.WriteLine(baratos.Count());
Console.WriteLine(baratos.Count());
Console.WriteLine($"O Where rodou {testes} vezes");

// (o record Produto)
$ dotnet run duasvezes.cs
3
3
O Where rodou 14 vezes      « 7 produtos × 2 contagens

Cada Count() refez a consulta inteira. Com 7 produtos não faz diferença, mas com uma consulta pesada (milhares de itens, ou dados lidos de um arquivo ou de um banco) é trabalho repetido. Se você vai usar o resultado várias vezes, guarde-o com ToList().

Adiados (só montam a receita)Imediatos (rodam na hora)
Where, Select, OrderBy, ThenBy, GroupBy, Take, Skip, Distinct…
Devolvem um IEnumerable<T>.
ToList, ToArray, ToDictionary, Count, Sum, Average, Min, Max, First, Any…
Devolvem uma coleção pronta ou um valor.
Quando usar ToList()
  • Quando você vai percorrer o resultado mais de uma vez.
  • Quando quer o resultado daquele momento, mesmo que a lista original mude depois.
  • Quando vai mudar a lista original enquanto percorre o resultado (veja o erro abaixo).

Fora desses casos, deixe a consulta adiada: ela não gasta memória com listas intermediárias.


Quando dá errado

InvalidOperationException: mudar a lista enquanto a consulta a percorre

Este código tem um erro de propósito. A Ana quer tirar da lista os produtos sem estoque:

// (a lista de produtos, linhas 1 a 10)

var semEstoque = produtos.Where(p => p.Estoque == 0);
foreach (Produto p in semEstoque)
{
    produtos.Remove(p);
}
Console.WriteLine(produtos.Count);

// (o record Produto)
$ dotnet run modificada.cs
Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
   at System.Collections.Generic.List`1.Enumerator.MoveNext()
   at System.Linq.Enumerable.SizeOptIListWhereIterator`1.MoveNext()
   at Program.<Main>$(String[] args) in /home/ana/cs/modificada.cs:line 13

O que quer dizer: "a coleção foi modificada; a enumeração não pode continuar". Por causa da execução adiada, o foreach está percorrendo a própria lista produtos (através do Where). Quando o Remove tira o Estojo, a lista muda no meio do caminho, e o próximo passo do foreach (o MoveNext, "mover para o próximo") falha.

Como consertar: fotografe antes de mexer, produtos.Where(p => p.Estoque == 0).ToList(): o foreach passa a percorrer a lista nova, e a original pode mudar à vontade. Ou, mais simples: produtos.RemoveAll(p => p.Estoque == 0); (veja Lambdas).

CS0742: consulta sem select

Este código tem um erro de propósito.

// (a lista de produtos, linhas 1 a 10)

var consulta =
    from p in produtos
    where p.Preco < 20m;

// (o record Produto)
$ dotnet run semselect.cs
/home/ana/cs/semselect.cs(14,24): error CS0742: O corpo de uma consulta deve terminar com uma cláusula select ou group

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que quer dizer: na sintaxe de consulta, o fim é obrigatório: select (o que entra no resultado) ou group. Na sintaxe de método, o .Select é opcional.

Como consertar: where p.Preco < 20m select p; (para ficar com os produtos inteiros) ou select p.Nome;.

CS1936: a fonte não é uma coleção

Este código tem um erro de propósito.

int estoque = 12;
var r = from x in estoque select x;
$ dotnet run naolista.cs
/home/ana/cs/naolista.cs(2,19): error CS1936: Não foi possível encontrar uma implementação do padrão de consulta para o tipo de origem "int". "Select" não encontrado.

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que quer dizer: o LINQ precisa de uma sequência (lista, array, IEnumerable<T>…). Um int é um número só, e não tem Select.

Como consertar: use uma coleção como fonte: from p in produtos ….


Referência

CódigoO que fazTipo
fonte.Where(x => condição)Filtra.adiado
fonte.Select(x => valor)Transforma cada elemento.adiado
fonte.OrderBy(x => chave)Ordena.adiado
fonte.GroupBy(x => chave)Agrupa.adiado
fonte.Count(), .Sum(…), .Average(…)Conta, soma, tira a média.imediato
fonte.ToList() / .ToArray()Roda a consulta e guarda o resultado.imediato
from x in fonte where … orderby … select …A mesma consulta, na sintaxe de consulta.adiado

A tabela completa está em Referência: LINQ.


Exercícios

1. A fonte da consulta

Complete a primeira linha da consulta, sobre a lista produtos, chamando cada elemento de p:

where p.Preco < 10m select p.Nome;

2. Traduzir para método

Escreva em sintaxe de método: from p in produtos where p.Estoque > 0 select p.Nome

produtos

3. Fotografar

Qual método faz a consulta rodar na hora e guarda o resultado numa List? (escreva com os parênteses)

4. var caros = produtos.Where(p => p.Preco > 100m); Quando os produtos são testados?

5. A consulta foi criada com 7 produtos na lista. Depois, a Ana acrescentou um oitavo, e só então fez o foreach. Quantos produtos a consulta testa?

6. Qual linha não é adiada?

Missão final: a Ana quer comparar os produtos baratos antes e depois de cadastrar a Caneta. Mas, por causa da execução adiada, as duas linhas saem iguais (rode e veja). Faça a consulta antes rodar na hora, com ToList(). A saída deve ser Antes: Caderno, Lápis, Borracha e Depois: Caderno, Lápis, Borracha, Caneta.

List<Produto> produtos =
[
    new("Caderno", "Papelaria", 18.90m, 12),
    new("Lápis", "Papelaria", 2.50m, 40),
    new("Mochila", "Bolsas", 129.90m, 3),
    new("Borracha", "Papelaria", 1.75m, 25),
    new("Estojo", "Bolsas", 24.90m, 0),
    new("Garrafa", "Cozinha", 35.00m, 8),
    new("Caneca", "Cozinha", 22.50m, 5),
];

var antes = produtos.Where(p => p.Preco < 20m);

produtos.Add(new("Caneta", "Papelaria", 3.00m, 30));
var depois = produtos.Where(p => p.Preco < 20m);

Console.WriteLine($"Antes:  {string.Join(", ", antes.Select(p => p.Nome))}");
Console.WriteLine($"Depois: {string.Join(", ", depois.Select(p => p.Nome))}");

record Produto(string Nome, string Categoria, decimal Preco, int Estoque);
List<Produto> produtos =
[
    new("Caderno", "Papelaria", 18.90m, 12),
    new("Lápis", "Papelaria", 2.50m, 40),
    new("Mochila", "Bolsas", 129.90m, 3),
    new("Borracha", "Papelaria", 1.75m, 25),
    new("Estojo", "Bolsas", 24.90m, 0),
    new("Garrafa", "Cozinha", 35.00m, 8),
    new("Caneca", "Cozinha", 22.50m, 5),
];

var antes = produtos.Where(p => p.Preco < 20m).ToList();

produtos.Add(new("Caneta", "Papelaria", 3.00m, 30));
var depois = produtos.Where(p => p.Preco < 20m);

Console.WriteLine($"Antes:  {string.Join(", ", antes.Select(p => p.Nome))}");
Console.WriteLine($"Depois: {string.Join(", ", depois.Select(p => p.Nome))}");

record Produto(string Nome, string Categoria, decimal Preco, int Estoque);

Resumo

  • LINQ é o jeito de perguntar a uma coleção: você diz o que quer (Where, OrderBy, Select…), cada um com uma lambda, e ele faz o laço.
  • Duas sintaxes, mesmo resultado: método (produtos.Where(p => …).Select(p => …), a mais usada) e consulta (from p in produtos where … select …, que termina com select ou group).
  • O LINQ não muda a fonte; ele produz um IEnumerable<T> novo.
  • Execução adiada: Where, Select, OrderBy só montam a receita. A consulta roda quando é percorrida, com a fonte como estiver naquela hora, e roda de novo a cada vez.
  • ToList(), Count(), Sum() e companhia rodam na hora. Use ToList() para usar o resultado várias vezes, para fotografar um momento ou antes de mudar a lista original (senão, InvalidOperationException).

Próxima aula: Filtrar e Projetar (Where, Select).