C# LINQ Count, Sum e Average
Muitas perguntas sobre uma lista têm como resposta um número só: quantos produtos estão em falta? Quantas unidades há no estoque? Qual o preço médio? Qual o mais caro? Juntar todos os elementos num único valor se chama agregar, e o LINQ tem um método pronto para cada conta comum: Count, Sum, Average, Min, Max, e o Aggregate para as contas que você mesmo inventar.
Count(): contar todos, ou só os que passam num teste.SumeAverage, e por que a média deinté umdouble.MineMax(o menor e o maior valor) ×MinByeMaxBy(o elemento).Aggregate: a conta "faça você mesmo", com e sem valor inicial.AnyeAll: perguntas de sim ou não sobre a lista toda.- O que cada um faz numa lista vazia, e o
Sumque estoura. - Os erros reais:
InvalidOperationException,OverflowExceptioneCS1503.
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 (linhas 1 a 10 de cada arquivo) e o record Produto no fim; os arquivos abaixo 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: o fechamento do caixa
No fim do dia, a Ana não quer a lista de todas as vendas; quer o resumo: "foram 37 vendas, R$ 1.200 no total, R$ 32 de média, a maior foi de R$ 129,90". Cada número desses junta a lista inteira num valor só. Até agora, o LINQ devolvia sequências (Where, Select, OrderBy, GroupBy); os métodos desta aula devolvem um valor e, por isso, rodam na hora (não são adiados).
| Pergunta | Método | Devolve |
|---|---|---|
| Quantos? | Count() | int |
| Quanto ao todo? | Sum(…) | o tipo somado |
| Quanto, em média? | Average(…) | decimal ou double |
| Qual o menor / maior valor? | Min(…) / Max(…) | o valor |
| Qual o elemento com o menor / maior valor? | MinBy(…) / MaxBy(…) | o elemento |
| Outra conta qualquer | Aggregate(…) | o que a conta produzir |
1. Contar e somar: Count e Sum
// (a lista de produtos)
int total = produtos.Count();
int emFalta = produtos.Count(p => p.Estoque == 0);
int unidades = produtos.Sum(p => p.Estoque);
decimal valor = produtos.Sum(p => p.Preco * p.Estoque);
Console.WriteLine($"Produtos: {total}");
Console.WriteLine($"Em falta: {emFalta}");
Console.WriteLine($"Unidades: {unidades}");
Console.WriteLine($"Valor do estoque: {valor:C}");
// (o record Produto)
$ dotnet run contar.cs Produtos: 7 Em falta: 1 Unidades: 93 Valor do estoque: R$ 1.152,75
- Count()
- Conta os elementos. Sem lambda, conta todos.
- Count(p => p.Estoque == 0)
- Com uma lambda de teste (como a do
Where), conta só os que passam. É o mesmo queprodutos.Where(p => p.Estoque == 0).Count(), numa chamada só. - Sum(p => p.Estoque)
- Soma o valor que a lambda devolve para cada produto. Como
Estoqueéint, a soma éint. - Sum(p => p.Preco * p.Estoque)
- A lambda pode fazer uma conta: aqui, o valor de cada produto no estoque. Com
decimal, a soma édecimal, exata até o centavo.
Para uma lista de números, o Sum() vai sem lambda: new[] { 10m, 20m }.Sum() dá 30. O mesmo vale para Average, Min e Max.
Missão: troque os zeros por LINQ. estoqueBaixo deve contar os produtos com menos de 10 unidades (use Count com uma condição), e unidades deve somar o estoque de todos (Sum). A saída deve ser Estoque baixo: 4 e Unidades: 93.
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),
];
int estoqueBaixo = 0;
int unidades = 0;
Console.WriteLine($"Estoque baixo: {estoqueBaixo}");
Console.WriteLine($"Unidades: {unidades}");
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),
];
int estoqueBaixo = produtos.Count(p => p.Estoque < 10);
int unidades = produtos.Sum(p => p.Estoque);
Console.WriteLine($"Estoque baixo: {estoqueBaixo}");
Console.WriteLine($"Unidades: {unidades}");
record Produto(string Nome, string Categoria, decimal Preco, int Estoque);
2. A média: Average
// (a lista de produtos)
decimal mediaPreco = produtos.Average(p => p.Preco);
double mediaEstoque = produtos.Average(p => p.Estoque);
int divisaoInteira = produtos.Sum(p => p.Estoque) / produtos.Count();
Console.WriteLine(mediaPreco);
Console.WriteLine($"{mediaPreco:C}");
Console.WriteLine(mediaEstoque);
Console.WriteLine($"{mediaEstoque:N1}");
Console.WriteLine(divisaoInteira);
// (o record Produto)
$ dotnet run media.cs 33,635714285714285714285714286 R$ 33,64 13,285714285714286 13,3 13
- Média de
decimalédecimal: 235,45 ÷ 7 = 33,6357…, com as 28 casas dodecimal. Para mostrar, formate (:Carredonda paraR$ 33,64) ou arredonde comMath.Round(media, 2). - Média de
intédouble: a média de 12, 40, 3, 25, 0, 8 e 5 é 13,2857…, que não cabe num inteiro. OAveragejá devolvedouble, para não perder a parte quebrada.:N1mostra com uma casa. - A conta "na mão" engana:
Sum(…) / Count()divide umintpor outroint, e a divisão inteira joga fora a parte quebrada:93 / 7dá13(veja Operadores Aritméticos). Prefira oAverage.
3. O menor e o maior: Min, Max, MinBy, MaxBy
// (a lista de produtos)
decimal menor = produtos.Min(p => p.Preco);
decimal maior = produtos.Max(p => p.Preco);
Produto? maisBarato = produtos.MinBy(p => p.Preco);
Produto? maisCaro = produtos.MaxBy(p => p.Preco);
Console.WriteLine($"Menor preço: {menor:C}");
Console.WriteLine($"Maior preço: {maior:C}");
Console.WriteLine($"Mais barato: {maisBarato?.Nome}");
Console.WriteLine($"Mais caro: {maisCaro?.Nome}");
Console.WriteLine(produtos.Max(p => p.Nome));
// (o record Produto)
$ dotnet run minmax.cs Menor preço: R$ 1,75 Maior preço: R$ 129,90 Mais barato: Borracha Mais caro: Mochila Mochila « o maior NOME em ordem alfabética (M vem depois de todos os outros)
| Método | Devolve | Exemplo |
|---|---|---|
Min(p => p.Preco) | o menor preço (um decimal) | 1,75 |
Max(p => p.Preco) | o maior preço | 129,90 |
MinBy(p => p.Preco) .NET 6 | o produto com o menor preço | a Borracha |
MaxBy(p => p.Preco) .NET 6 | o produto com o maior preço | a Mochila |
Com Max, você sabe quanto custa o mais caro, mas não qual é. Com MaxBy, você recebe o produto inteiro (nome, categoria, estoque). O ? em Produto? existe porque, numa lista vazia, MinBy e MaxBy devolvem null (veja a seção 6). Antes do .NET 6, o jeito era produtos.OrderByDescending(p => p.Preco).First(), que ordena a lista toda só para pegar um.
Min e Max funcionam com qualquer coisa comparável: com textos, Max(p => p.Nome) dá o último em ordem alfabética.
4. A conta "faça você mesmo": Aggregate
Todos os métodos anteriores são casos especiais de uma ideia: começar com um valor e ir acumulando elemento por elemento. O Aggregate ("agregar") deixa você escrever a regra do acúmulo:
// (a lista de produtos)
string lista = produtos
.Select(p => p.Nome)
.Aggregate((acumulado, nome) => acumulado + " + " + nome);
Console.WriteLine(lista);
decimal total = produtos.Aggregate(0m, (soma, p) => soma + p.Preco);
Console.WriteLine(total);
string resumo = produtos.Aggregate(
0,
(unidades, p) => unidades + p.Estoque,
unidades => $"{unidades} unidades no estoque");
Console.WriteLine(resumo);
decimal comDescontos = new[] { 10, 5 }.Aggregate(100m, (preco, desconto) => preco - preco * desconto / 100);
Console.WriteLine(comDescontos);
// (o record Produto)
$ dotnet run aggregate.cs Caderno + Lápis + Mochila + Borracha + Estojo + Garrafa + Caneca 235,45 93 unidades no estoque 85,5
- Aggregate((acumulado, nome) => …)
- Sem valor inicial: o primeiro elemento ("Caderno") é o começo. A lambda recebe o que foi acumulado até ali e o próximo elemento, e devolve o novo acumulado:
"Caderno"→"Caderno + Lápis"→"Caderno + Lápis + Mochila"… (para juntar textos, ostring.Joinfaz o mesmo mais rápido). - Aggregate(0m, (soma, p) => …)
- Com valor inicial (a "semente", seed): começa em
0me soma o preço de cada produto. É exatamente o queSum(p => p.Preco)faz. O acumulado pode ser de outro tipo que o elemento (decimal×Produto). - Aggregate(0, …, unidades => …)
- Com uma terceira lambda, que transforma o resultado final: soma o estoque e, no fim, monta o texto.
- new[] { 10, 5 }.Aggregate(100m, …)
- Um uso que nenhum método pronto faz: aplicar descontos em sequência. R$ 100 com 10% vira 90; com mais 5%, vira 85,5. (Não é 15% direto, que daria 85.)
Se existe um método pronto (Count, Sum, Average, Min, Max, string.Join), use-o: ele diz o que faz no nome e é mais rápido. Deixe o Aggregate para contas que não têm nome pronto, como os descontos em sequência. E prefira sempre a versão com valor inicial, que funciona também numa lista vazia.
5. Perguntas de sim ou não: Any e All
Às vezes você não quer um número, mas um "sim" ou "não" sobre a lista toda:
// (a lista de produtos) Console.WriteLine(produtos.Any(p => p.Estoque == 0)); Console.WriteLine(produtos.All(p => p.Preco > 1m)); // (o record Produto)
$ dotnet run any.cs True « existe ALGUM produto em falta (o Estojo) True « TODOS custam mais de R$ 1
Any ("algum") para no primeiro que passa; All ("todos") para no primeiro que falha. Por isso, para saber "tem algum?", produtos.Any(p => p.Estoque == 0) é melhor que produtos.Count(p => p.Estoque == 0) > 0: o Count precisa olhar a lista inteira, e o Any não.
6. E se a lista estiver vazia?
A loja não vende brinquedos. O que cada método responde sobre uma sequência vazia?
Este código tem um erro de propósito (na última linha).
// (a lista de produtos, linhas 1 a 10)
var brinquedos = produtos.Where(p => p.Categoria == "Brinquedos");
Console.WriteLine($"Count: {brinquedos.Count()}");
Console.WriteLine($"Sum: {brinquedos.Sum(p => p.Preco)}");
Console.WriteLine($"MaxBy: {brinquedos.MaxBy(p => p.Preco)?.Nome ?? "(nenhum)"}");
Console.WriteLine($"Max com decimal?: {brinquedos.Max(p => (decimal?)p.Preco) ?? 0m}");
Console.WriteLine($"Average: {brinquedos.Average(p => p.Preco)}");
// (o record Produto)
$ dotnet run vazio.cs Count: 0 Sum: 0 MaxBy: (nenhum) Max com decimal?: 0 Unhandled exception. System.InvalidOperationException: Sequence contains no elements at System.Linq.ThrowHelper.ThrowNoElementsException() at System.Linq.Enumerable.Average[TSource,TSelector,TAccumulator,TResult](IEnumerable`1 source, Func`2 selector) at Program.<Main>$(String[] args) in /home/ana/cs/vazio.cs:line 18
| Numa sequência vazia… | Resultado |
|---|---|
Count() | 0 |
Sum(…) | 0 |
Any(…) / All(…) | false / true ("todos os zero brinquedos custam mais de R$ 1" é verdade) |
MinBy(…) / MaxBy(…) de objetos | null |
Min / Max / Average de decimal?, int?… | null |
Min / Max / Average de decimal, int, double | InvalidOperationException |
Aggregate sem valor inicial | InvalidOperationException |
Aggregate com valor inicial | o valor inicial |
Somar nada dá zero, e contar nada também. Mas qual é a média de nenhum preço? Não existe, e o .NET não inventa um número: ele lança a exceção (veja abaixo). O truque (decimal?)p.Preco converte cada preço num decimal anulável (veja null e Tipos Anuláveis); assim, numa lista vazia, o Max devolve null em vez de falhar, e o ?? 0m troca o null por zero.
Quando dá errado
Visto na seção 6: Average, Min e Max de números (e o Aggregate sem semente) numa sequência vazia. Em português: "a sequência não contém elementos". O código de saída é 134. A mesma exceção aparece com o Aggregate:
// (a lista de produtos, linhas 1 a 10) string nomes = produtos.Where(p => p.Preco > 1000m).Select(p => p.Nome).Aggregate((a, b) => a + ", " + b); // (o record Produto)
$ dotnet run agg-vazio.cs Unhandled exception. System.InvalidOperationException: Sequence contains no elements at System.Linq.ThrowHelper.ThrowNoElementsException() at System.Linq.Enumerable.Aggregate[TSource](IEnumerable`1 source, Func`3 func) at Program.<Main>$(String[] args) in /home/ana/cs/agg-vazio.cs:line 12
Como consertar: confira antes com Any() (var media = lista.Any() ? lista.Average(p => p.Preco) : 0m;), use a versão anulável (Average(p => (decimal?)p.Preco), que devolve null) ou, no Aggregate, dê um valor inicial. Para juntar textos, string.Join devolve "" numa lista vazia.
int
Este código tem um erro de propósito.
int[] vendasDoAno = [2_000_000_000, 2_000_000_000]; Console.WriteLine(vendasDoAno.Sum());
$ dotnet run overflow.cs Unhandled exception. System.OverflowException: Arithmetic operation resulted in an overflow. at System.Int32.System.Numerics.IAdditionOperators<System.Int32,System.Int32,System.Int32>.op_CheckedAddition(Int32 left, Int32 right) at System.Linq.Enumerable.Sum[T,TResult](ReadOnlySpan`1 span) at System.Linq.Enumerable.Sum[TSource,TResult](IEnumerable`1 source) at Program.<Main>$(String[] args) in /home/ana/cs/overflow.cs:line 2
O que quer dizer: "a operação aritmética resultou num estouro". 4 bilhões não cabem num int, que vai só até 2.147.483.647 (veja Inteiros). Um + comum, nesse caso, "daria a volta" em silêncio e mostraria um número negativo; o Sum do LINQ confere cada soma (CheckedAddition, "adição conferida") e prefere parar o programa a dar um total errado.
Como consertar: some num tipo maior, convertendo cada valor: vendasDoAno.Sum(v => (long)v) mostra 4000000000. Para dinheiro, use decimal desde o começo.
Count sem parênteses numa consulta
Este código tem um erro de propósito.
// (a lista de produtos, linhas 1 a 10) int n = produtos.Count; int m = produtos.Count(); var caros = produtos.Where(p => p.Preco > 20m); Console.WriteLine(caros.Count); // (o record Produto)
$ dotnet run count-prop.cs /home/ana/cs/count-prop.cs(15,19): error CS1503: Argumento 1: não é possível converter de "grupo de métodos" para "bool" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: as linhas 12 e 13 compilam: uma List tem a propriedade Count (sem parênteses) e também ganha o método Count() do LINQ. Mas caros é um IEnumerable<Produto>, que só tem o método. Sem os parênteses, caros.Count é o método em si (um "grupo de métodos", como em Delegates), e o Console.WriteLine não sabe escrever isso; a mensagem cita bool porque o compilador tentou, sem sucesso, cada versão do WriteLine.
Como consertar: caros.Count(). Regra prática: numa List ou num array, a propriedade (lista.Count, array.Length) é direta; num resultado do LINQ, use o método Count().
Referência
| Código | Devolve | Lista vazia |
|---|---|---|
Count() / Count(p => teste) | quantos (int; LongCount para long) | 0 |
Sum(p => valor) | a soma (confere estouro) | 0 |
Average(p => valor) | a média (decimal → decimal; int → double) | exceção |
Min(p => valor) / Max(p => valor) | o menor / maior valor | exceção (ou null com T?) |
MinBy(p => chave) / MaxBy(p => chave) | o elemento com a menor / maior chave | null |
Aggregate((acc, x) => …) | o acumulado, começando pelo 1º elemento | exceção |
Aggregate(semente, (acc, x) => …) | o acumulado, começando pela semente | a semente |
Any(p => teste) / All(p => teste) | algum passa? / todos passam? | false / true |
Na sintaxe de consulta não há palavras para agregar: ponha a consulta entre parênteses e chame o método, (from p in produtos where p.Estoque > 0 select p).Count().
Exercícios
1. Contar com condição
Complete para contar os produtos da categoria "Bolsas":
2. O produto mais caro
Complete para obter o produto (não só o preço) mais caro:
3. O tipo da média
Qual é o tipo devolvido por produtos.Average(p => p.Estoque), sabendo que Estoque é int?
4. new List<decimal>().Sum() e new List<decimal>().Average() dão…
5. Qual a diferença entre Max(p => p.Preco) e MaxBy(p => p.Preco)?
6. Para saber se existe algum produto em falta, o melhor é…
Missão final: complete o relatório da papelaria. Depois da linha que conta os produtos, mostre o preço médio (com Average e :C), o nome do mais barato (com MinBy) e o do mais caro (com MaxBy). A saída deve ser Preço médio: R$ 7,72, Mais barato: Borracha e Mais caro: Caderno.
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 papelaria = produtos.Where(p => p.Categoria == "Papelaria");
Console.WriteLine($"Produtos: {papelaria.Count()}");
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 papelaria = produtos.Where(p => p.Categoria == "Papelaria");
Console.WriteLine($"Produtos: {papelaria.Count()}");
Console.WriteLine($"Preço médio: {papelaria.Average(p => p.Preco):C}");
Console.WriteLine($"Mais barato: {papelaria.MinBy(p => p.Preco)?.Nome}");
Console.WriteLine($"Mais caro: {papelaria.MaxBy(p => p.Preco)?.Nome}");
record Produto(string Nome, string Categoria, decimal Preco, int Estoque);
Repare que a consulta papelaria é adiada e foi percorrida quatro vezes (uma por método). Com 7 produtos tanto faz; numa lista grande, um .ToList() no fim da primeira linha faria o Where rodar uma vez só.
Resumo
- Agregar é juntar a sequência inteira num valor. Esses métodos rodam na hora.
Count()conta (com lambda, só os que passam);Sumsoma (e confere estouro:OverflowException).Averagetira a média: dedecimalvemdecimal; deintvemdouble. Não dividaSumporCountcom inteiros.Min/Maxdão o valor;MinBy/MaxBydão o elemento.Aggregatefaz qualquer acúmulo; prefira a versão com semente.Any/Allrespondem sim ou não sobre a lista toda.- Lista vazia:
CounteSumdão 0;Average,MineMaxde números lançamInvalidOperationException("Sequence contains no elements"). UseAny()antes ou a versão anulável.
Próxima aula: Juntar (Join), para combinar duas listas, como produtos e vendas.