C# LINQ Where e Select
As duas operações mais usadas do LINQ são Where ("onde"), que filtra, ficando só com os elementos que passam num teste, e Select ("selecionar"), que projeta, transformando cada elemento em outra coisa: só o nome, o preço com desconto, uma etiqueta pronta. Juntas, elas respondem perguntas como "quais produtos estão em falta, e quanto custa repor cada um?".
- Filtrar com
Where: uma condição, várias (&&,||) e com um método pronto. - Encadear filtros e o que acontece quando nada passa.
- Projetar com
Select: uma propriedade, uma conta, um texto. - Projetar para tipos anônimos, tuplas e records, e numerar com o índice.
- Combinar
WhereeSelect, e oletda sintaxe de consulta. - Os erros reais: a ordem errada (
CS1061),=no lugar de==(CS8852) e o filtro que não devolvebool(CS0029).
Todos os programas rodaram com o .NET 10 (C# 14) na máquina da Ana, na pasta ~/cs, com dotnet run arquivo.cs. Eles usam a lista de produtos da lojinha apresentada em O que é LINQ; para não repetir, os arquivos mostram só o programa:
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 começa na linha 12 …
record Produto(string Nome, string Categoria, decimal Preco, int Estoque);
A ideia: a peneira e o carimbo
Na linha de montagem do LINQ, o Where é uma peneira: os produtos passam por ela, e só os que servem continuam. Ninguém é modificado; alguns só ficam para trás. O Select é um carimbo: cada produto que chega sai transformado em outra coisa (o nome dele, uma etiqueta de preço). Nenhum fica para trás, mas todos mudam de forma.
Where (peneira) | Select (carimbo) | |
|---|---|---|
| A lambda recebe | um produto | um produto |
| A lambda devolve | bool: "este passa?" | qualquer coisa: "vira o quê?" |
| Quantos saem | os que passaram (de nenhum a todos) | exatamente quantos entraram |
| O tipo que sai | o mesmo: IEnumerable<Produto> | o que a lambda devolve: IEnumerable<string>, IEnumerable<decimal>… |
1. Filtrar com Where
// (a lista de produtos)
var baratos = produtos.Where(p => p.Preco < 20m);
var papelariaComEstoque = produtos.Where(p => p.Categoria == "Papelaria" && p.Estoque > 20);
var bolsasOuCozinha = produtos.Where(p => p.Categoria == "Bolsas" || p.Categoria == "Cozinha");
var emFalta = produtos.Where(EstaEmFalta);
Mostrar("Baratos", baratos);
Mostrar("Papelaria com mais de 20", papelariaComEstoque);
Mostrar("Bolsas ou cozinha", bolsasOuCozinha);
Mostrar("Em falta", emFalta);
static bool EstaEmFalta(Produto p) => p.Estoque < 5;
static void Mostrar(string titulo, IEnumerable<Produto> lista) =>
Console.WriteLine($"{titulo}: {string.Join(", ", lista.Select(p => p.Nome))}");
// (o record Produto)
$ dotnet run where.cs Baratos: Caderno, Lápis, Borracha Papelaria com mais de 20: Lápis, Borracha Bolsas ou cozinha: Mochila, Estojo, Garrafa, Caneca Em falta: Mochila, Estojo
| Linha | O que faz |
|---|---|
Where(p => p.Preco < 20m) | A lambda recebe cada produto (p) e responde true ou false. Só os true continuam. |
… == "Papelaria" && p.Estoque > 20 | Duas condições com && ("e"): as duas precisam ser verdadeiras (veja Operadores Lógicos). O Caderno é de papelaria, mas tem só 12. |
… == "Bolsas" || … == "Cozinha" | Com || ("ou"), basta uma ser verdadeira. |
Where(EstaEmFalta) | No lugar da lambda, um método com nome que recebe um Produto e devolve bool (sem parênteses, como em Delegates). Bom quando o teste é usado em vários lugares ou é comprido. |
Mostrar(…) | Um método auxiliar que junta os nomes com string.Join. Ele recebe IEnumerable<Produto>, o tipo que o Where devolve. |
Repare que os resultados saem na ordem da lista original: o Where não muda a ordem, só tira elementos. Para mudar a ordem, existe o OrderBy.
Encadear filtros, e quando nada passa
// (a lista de produtos)
var resultado = produtos
.Where(p => p.Estoque > 0)
.Where(p => p.Preco >= 20m);
foreach (Produto p in resultado)
{
Console.WriteLine($"{p.Nome,-8} {p.Preco,10:C} {p.Estoque,3} un.");
}
var nada = produtos.Where(p => p.Preco > 1000m);
Console.WriteLine($"Acima de R$ 1000: {nada.Count()} produtos");
foreach (Produto p in nada)
{
Console.WriteLine("isto nunca aparece");
}
// (o record Produto)
$ dotnet run encadear.cs Mochila R$ 129,90 3 un. Garrafa R$ 35,00 8 un. Caneca R$ 22,50 5 un. Acima de R$ 1000: 0 produtos
- Dois
Whereseguidos equivalem a um só com&&: o segundo só vê quem passou no primeiro. O Estojo (estoque 0) caiu na primeira peneira. - O
{p.Nome,-8}e o{p.Preco,10:C}alinham as colunas: o número depois da vírgula é a largura, negativo para alinhar à esquerda (veja Formatar Números). - Quando ninguém passa, o
Wheredevolve uma sequência vazia, e nãonull: oforeachsimplesmente não roda e oCount()dá 0. Não é preciso testarnull.
Missão: o filtro pega todos os produtos de papelaria. Acrescente uma segunda condição para ficar só com os que custam menos de R$ 5. A saída deve ser só Lápis e Borracha.
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 escolhidos = produtos.Where(p => p.Categoria == "Papelaria");
foreach (Produto p in escolhidos)
{
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 escolhidos = produtos.Where(p => p.Categoria == "Papelaria" && p.Preco < 5m);
foreach (Produto p in escolhidos)
{
Console.WriteLine(p.Nome);
}
record Produto(string Nome, string Categoria, decimal Preco, int Estoque);
p.Categoria == "papelaria" (com p minúsculo) não encontra nada, porque "Papelaria" e "papelaria" são textos diferentes para o ==. Se o texto vem do usuário, compare ignorando maiúsculas: p.Categoria.Equals("papelaria", StringComparison.OrdinalIgnoreCase). No laboratório, a primeira forma deu 0 produtos e a segunda, 3 (veja Métodos de String).
2. Projetar com Select
O Select recebe cada elemento e devolve o que você quiser no lugar dele:
// (a lista de produtos)
IEnumerable<string> nomes = produtos.Select(p => p.Nome);
IEnumerable<decimal> comDesconto = produtos.Select(p => p.Preco * 0.9m);
IEnumerable<string> etiquetas = produtos.Select(p => $"{p.Nome.ToUpper()} {p.Preco:C}");
IEnumerable<decimal> valorEmEstoque = produtos.Select(p => p.Preco * p.Estoque);
Console.WriteLine(string.Join(", ", nomes));
Console.WriteLine(string.Join(" | ", comDesconto));
Console.WriteLine(string.Join(" | ", etiquetas.Take(3)));
Console.WriteLine(string.Join(" | ", valorEmEstoque));
// (o record Produto)
$ dotnet run select.cs Caderno, Lápis, Mochila, Borracha, Estojo, Garrafa, Caneca 17,010 | 2,250 | 116,910 | 1,575 | 22,410 | 31,500 | 20,250 CADERNO R$ 18,90 | LÁPIS R$ 2,50 | MOCHILA R$ 129,90 226,80 | 100,00 | 389,70 | 43,75 | 0,00 | 280,00 | 112,50
| Projeção | Cada produto vira… | Tipo do resultado |
|---|---|---|
p => p.Nome | só o nome | IEnumerable<string> |
p => p.Preco * 0.9m | o preço com 10% de desconto | IEnumerable<decimal> |
p => $"{p.Nome.ToUpper()} {p.Preco:C}" | uma etiqueta pronta | IEnumerable<string> |
p => p.Preco * p.Estoque | quanto vale o estoque daquele produto | IEnumerable<decimal> |
- Os 7 produtos viraram 7 valores: o
Selectnunca tira nem acrescenta elementos. 17,010tem três casas porque odecimalguarda as casas da conta (18,90 × 0,9: duas casas mais uma). Para dinheiro, mostre com:Cou arredonde comMath.Round(valor, 2).Take(3)("pegue 3") fica só com os 3 primeiros, para a linha não ficar comprida.
Várias informações de uma vez: tipo anônimo, tupla e record
E se você quer o nome e um valor calculado? O Select pode devolver um objeto com várias partes:
// (a lista de produtos)
var anonimos = produtos.Select(p => new { p.Nome, Total = p.Preco * p.Estoque });
var tuplas = produtos.Select(p => (p.Nome, p.Estoque));
var etiquetas = produtos.Select(p => new Etiqueta(p.Nome, Math.Round(p.Preco * 0.9m, 2)));
var numerados = produtos.Select((p, i) => $"{i + 1}. {p.Nome}");
Console.WriteLine(anonimos.First());
Console.WriteLine(anonimos.First().Total);
Console.WriteLine(tuplas.First());
Console.WriteLine(tuplas.First().Estoque);
Console.WriteLine(etiquetas.First());
Console.WriteLine(string.Join(" ", numerados));
record Etiqueta(string Texto, decimal Preco);
// (o record Produto)
$ dotnet run formas.cs
{ Nome = Caderno, Total = 226,80 }
226,80
(Caderno, 12)
12
Etiqueta { Texto = Caderno, Preco = 17,01 }
1. Caderno 2. Lápis 3. Mochila 4. Borracha 5. Estojo 6. Garrafa 7. Caneca
- new { p.Nome, Total = … }
- Um tipo anônimo: um objeto sem classe declarada, criado ali mesmo.
p.Nomevira a propriedadeNome(o nome vem junto);Total = …dá nome a um valor calculado. Como o tipo não tem nome, a variável precisa servar. OFirst()pega o primeiro elemento. - (p.Nome, p.Estoque)
- Uma tupla, com as partes
NomeeEstoque. Mais leve que o tipo anônimo, e pode ser devolvida por métodos. - new Etiqueta(…)
- Um record seu. Use quando o resultado vai circular pelo programa (ser devolvido, guardado, passado para outros métodos).
- (p, i) => …
- Com dois parâmetros, o
Selectentrega também a posição (o índice, que começa em 0) de cada elemento. OWheretem a mesma versão:Where((p, i) => i % 2 == 0)pega um sim, um não.
Para usar o resultado ali mesmo, num foreach logo abaixo: tipo anônimo (é o mais comum em LINQ). Para devolver de um método: tupla ou record. Para algo que tem nome e significado no programa (uma etiqueta, um item de relatório): record.
3. Where e Select juntos
O uso mais comum: filtrar primeiro, depois projetar. Abaixo, a promoção da Ana dá 20% de desconto nos produtos com mais de 10 unidades, nas duas sintaxes:
// (a lista de produtos)
var promocao = produtos
.Where(p => p.Estoque > 10)
.Select(p => new { p.Nome, De = p.Preco, Por = p.Preco * 0.8m });
foreach (var item in promocao)
{
Console.WriteLine($"{item.Nome}: de {item.De:C} por {item.Por:C}");
}
var consulta =
from p in produtos
where p.Estoque > 10
let promo = p.Preco * 0.8m
select $"{p.Nome}: {promo:C}";
Console.WriteLine(string.Join(" | ", consulta));
// (o record Produto)
$ dotnet run juntos.cs Caderno: de R$ 18,90 por R$ 15,12 Lápis: de R$ 2,50 por R$ 2,00 Borracha: de R$ 1,75 por R$ 1,40 Caderno: R$ 15,12 | Lápis: R$ 2,00 | Borracha: R$ 1,40
- No
foreach (var item in promocao), ovaré obrigatório:itemé do tipo anônimo, que não tem nome para escrever. - Na sintaxe de consulta, o
let("seja") cria uma variável no meio da consulta, para não repetir a conta. Na sintaxe de método, o equivalente é umSelectcom tipo anônimo, como o de cima.
A ordem importa: o Where vem antes do Select porque precisa do produto inteiro para testar. Depois de .Select(p => p.Nome), só sobram textos, e não há mais preço para filtrar (veja o erro CS1061 logo abaixo).
Quando dá errado
Select antes do Where
Este código tem um erro de propósito.
// (a lista de produtos, linhas 1 a 10)
var nomes = produtos
.Select(p => p.Nome)
.Where(p => p.Preco < 20m);
// (o record Produto)
$ dotnet run ordem-erro.cs /home/ana/cs/ordem-erro.cs(14,19): error CS1061: ‘string’ não contém uma definição para "Preco" e não foi possível encontrar nenhum método de extensão "Preco" que aceite um primeiro argumento do tipo ‘string’ (você está se esquecendo de usar uma diretiva ou uma referência de assembly?) Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: depois do Select(p => p.Nome), a sequência é de string. No Where, o p já é um texto (o nome), e texto não tem Preco. O nome p engana: ele não é mais um produto.
Como consertar: filtre antes de projetar: .Where(p => p.Preco < 20m).Select(p => p.Nome).
= no lugar de ==
Este código tem um erro de propósito.
// (a lista de produtos, linhas 1 a 10) var dez = produtos.Where(p => p.Preco = 18.90m); // (o record Produto)
$ dotnet run igual-erro.cs /home/ana/cs/igual-erro.cs(12,31): error CS8852: A propriedade somente de inicialização ou o indexador 'Produto.Preco' só pode ser atribuído em um inicializador de objeto, em 'this' ou 'base' em um construtor de instância ou em um acessador 'init'. Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: um = só é atribuição ("guarde 18,90 no preço"), não comparação. O compilador reclama de outra coisa: as propriedades de um record são init e não podem ser mudadas depois de criadas (veja readonly e init). A mensagem confunde, mas o erro está no sinal.
Como consertar: para comparar, use ==: Where(p => p.Preco == 18.90m) (veja Operadores de Comparação).
bool
Este código tem um erro de propósito.
// (a lista de produtos, linhas 1 a 10) var nomes = produtos.Where(p => p.Nome); // (o record Produto)
$ dotnet run bool-erro.cs /home/ana/cs/bool-erro.cs(12,33): error CS0029: Não é possível converter implicitamente tipo "string" em "bool" /home/ana/cs/bool-erro.cs(12,33): error CS1662: Não é possível converter expressão lambda para o tipo delegate pretendido porque alguns dos tipos de retorno no bloco não são implicitamente conversíveis para o tipo de retorno delegate Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: o Where pede um Func<Produto, bool>, um teste de sim ou não, e a lambda devolve o nome (um string). Quem queria "só os nomes" se confundiu de método.
Como consertar: para pegar os nomes, use Select(p => p.Nome). Para filtrar, devolva um bool: Where(p => p.Nome.StartsWith('C')).
Referência
| Código | O que faz |
|---|---|
Where(p => teste) | Fica com os elementos em que o teste é true, na ordem original. |
Where(Metodo) | O mesmo, com um método bool Metodo(T x). |
Where((p, i) => teste) | O teste recebe também a posição i. |
Select(p => valor) | Troca cada elemento pelo valor (mesma quantidade). |
Select(p => new { p.Nome, X = … }) | Projeta para um tipo anônimo (use var). |
Select(p => (p.Nome, p.Preco)) | Projeta para uma tupla. |
Select((p, i) => …) | A projeção recebe também a posição. |
where teste / select valor / let x = … | As mesmas operações na sintaxe de consulta. |
Exercícios
1. Filtrar
Complete para ficar só com os produtos que têm estoque (mais de zero):
2. Projetar
Complete para obter os preços de todos os produtos:
3. Tipo anônimo
Complete o tipo anônimo com o nome do produto e uma propriedade Dobro com o dobro do preço:
4. A lista tem 7 produtos. Quantos elementos produtos.Select(p => p.Nome) produz?
5. O que produtos.Where(p => p.Preco > 1000m) devolve quando nenhum produto passa?
6. Por que .Select(p => p.Nome).Where(p => p.Preco < 20m) não compila?
Missão final: faça as etiquetas da promoção da cozinha. Com Where, fique só com a categoria "Cozinha"; com Select, transforme cada produto no texto "{Nome} por {preço com 10% de desconto:C}" (o preço vezes 0.9m). A saída deve ser exatamente Garrafa por R$ 31,50 e Caneca por R$ 20,25.
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 etiquetas = produtos.Select(p => p.Nome);
foreach (string e in etiquetas)
{
Console.WriteLine(e);
}
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 etiquetas = produtos
.Where(p => p.Categoria == "Cozinha")
.Select(p => $"{p.Nome} por {p.Preco * 0.9m:C}");
foreach (string e in etiquetas)
{
Console.WriteLine(e);
}
record Produto(string Nome, string Categoria, decimal Preco, int Estoque);
Resumo
Whereé a peneira: a lambda devolvebool, e só ostruecontinuam, na ordem original. Combine condições com&&e||, ou encadeie váriosWhere.- Sem ninguém que passe, o resultado é uma sequência vazia, nunca
null. Selecté o carimbo: cada elemento vira o que a lambda devolver, e a quantidade não muda.- Para várias informações: tipo anônimo (
new { p.Nome, Total = … }, comvar), tupla ou record. Com(p, i), você recebe também a posição. - Filtre antes de projetar; depois do
Select, o elemento já é outra coisa (CS1061). - Compare com
==, não=(CS8852num record); oWhereprecisa de um testebool(CS0029).
Próxima aula: Ordenar (OrderBy).