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.
- 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,CS1936e aInvalidOperationExceptionde 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:
| Linha | Em português | Aula |
|---|---|---|
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
pemprodutos": a fonte, e o nome de cada elemento (é opdas lambdas). Toda consulta começa comfrom. - 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 comselectougroup(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 consulta | Sintaxe de método |
|---|---|
from p in produtos | produtos |
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)… |
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
WhereeSelectdevolvem umIEnumerable<T>("enumerável de T"): uma sequência que dá para percorrer comforeach. OWheredevolve produtos; oSelect(p => p.Nome)transforma em textos, por issoIEnumerable<string>.ToList()percorre a sequência e guarda tudo numaList<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 dostring.Join, quando a Borracha já estava na lista.fotografiarodou na hora, por causa doToList(), quando a lista tinha só dois produtos. Ela é umaListnova, 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:
- Entre o "1)" e o "2)", nada rodou: montar a consulta não testa produto nenhum.
- Os produtos passam pela linha de montagem um de cada vez: o Caderno vai do
WhereaoSelecte chega aoforeachantes de o Lápis ser testado. O LINQ não cria uma lista intermediária com "os que passaram noWhere".
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. |
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
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).
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;.
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ódigo | O que faz | Tipo |
|---|---|---|
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:
2. Traduzir para método
Escreva em sintaxe de método: from p in produtos where p.Estoque > 0 select p.Nome
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 comselectougroup). - O LINQ não muda a fonte; ele produz um
IEnumerable<T>novo. - Execução adiada:
Where,Select,OrderBysó 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. UseToList()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).