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?".

Você vai aprender
  • 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 Where e Select, e o let da sintaxe de consulta.
  • Os erros reais: a ordem errada (CS1061), = no lugar de == (CS8852) e o filtro que não devolve bool (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 recebeum produtoum produto
A lambda devolvebool: "este passa?"qualquer coisa: "vira o quê?"
Quantos saemos que passaram (de nenhum a todos)exatamente quantos entraram
O tipo que saio 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
LinhaO 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 > 20Duas 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 Where seguidos 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 Where devolve uma sequência vazia, e não null: o foreach simplesmente não roda e o Count() dá 0. Não é preciso testar null.

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);
Maiúsculas e minúsculas contam

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çãoCada produto vira…Tipo do resultado
p => p.Nomesó o nomeIEnumerable<string>
p => p.Preco * 0.9mo preço com 10% de descontoIEnumerable<decimal>
p => $"{p.Nome.ToUpper()} {p.Preco:C}"uma etiqueta prontaIEnumerable<string>
p => p.Preco * p.Estoquequanto vale o estoque daquele produtoIEnumerable<decimal>
  • Os 7 produtos viraram 7 valores: o Select nunca tira nem acrescenta elementos.
  • 17,010 tem três casas porque o decimal guarda as casas da conta (18,90 × 0,9: duas casas mais uma). Para dinheiro, mostre com :C ou arredonde com Math.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.Nome vira a propriedade Nome (o nome vem junto); Total = … dá nome a um valor calculado. Como o tipo não tem nome, a variável precisa ser var. O First() pega o primeiro elemento.
(p.Nome, p.Estoque)
Uma tupla, com as partes Nome e Estoque. 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 Select entrega também a posição (o índice, que começa em 0) de cada elemento. O Where tem a mesma versão: Where((p, i) => i % 2 == 0) pega um sim, um não.
Qual usar

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), o var é 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 é um Select com 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

CS1061: 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).

CS8852: = 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).

CS0029 + CS1662: o filtro não devolve 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ódigoO 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):

var comEstoque = produtos.Where();

2. Projetar

Complete para obter os preços de todos os produtos:

var precos = 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:

produtos.Select(p => new { p.Nome, })

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 devolve bool, e só os true continuam, na ordem original. Combine condições com && e ||, ou encadeie vários Where.
  • 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 = … }, com var), 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 = (CS8852 num record); o Where precisa de um teste bool (CS0029).

Próxima aula: Ordenar (OrderBy).