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.

Você vai aprender
  • Count(): contar todos, ou só os que passam num teste.
  • Sum e Average, e por que a média de int é um double.
  • Min e Max (o menor e o maior valor) × MinBy e MaxBy (o elemento).
  • Aggregate: a conta "faça você mesmo", com e sem valor inicial.
  • Any e All: perguntas de sim ou não sobre a lista toda.
  • O que cada um faz numa lista vazia, e o Sum que estoura.
  • Os erros reais: InvalidOperationException, OverflowException e CS1503.

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

PerguntaMétodoDevolve
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 qualquerAggregate(…)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 que produtos.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 do decimal. Para mostrar, formate (:C arredonda para R$ 33,64) ou arredonde com Math.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. O Average já devolve double, para não perder a parte quebrada. :N1 mostra com uma casa.
  • A conta "na mão" engana: Sum(…) / Count() divide um int por outro int, e a divisão inteira joga fora a parte quebrada: 93 / 7 dá 13 (veja Operadores Aritméticos). Prefira o Average.

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étodoDevolveExemplo
Min(p => p.Preco)o menor preço (um decimal)1,75
Max(p => p.Preco)o maior preço129,90
MinBy(p => p.Preco) .NET 6o produto com o menor preçoa Borracha
MaxBy(p => p.Preco) .NET 6o produto com o maior preçoa 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, o string.Join faz o mesmo mais rápido).
Aggregate(0m, (soma, p) => …)
Com valor inicial (a "semente", seed): começa em 0m e soma o preço de cada produto. É exatamente o que Sum(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.)
Qual usar

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 objetosnull
Min / Max / Average de decimal?, int?…null
Min / Max / Average de decimal, int, doubleInvalidOperationException
Aggregate sem valor inicialInvalidOperationException
Aggregate com valor inicialo 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

InvalidOperationException: "Sequence contains no elements"

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.

OverflowException: a soma não cabe no 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.

CS1503: 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ódigoDevolveLista 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 valorexceção (ou null com T?)
MinBy(p => chave) / MaxBy(p => chave)o elemento com a menor / maior chavenull
Aggregate((acc, x) => …)o acumulado, começando pelo 1º elementoexceção
Aggregate(semente, (acc, x) => …)o acumulado, começando pela sementea 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":

int bolsas = produtos.;

2. O produto mais caro

Complete para obter o produto (não só o preço) mais caro:

Produto? maisCaro = produtos.;

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); Sum soma (e confere estouro: OverflowException).
  • Average tira a média: de decimal vem decimal; de int vem double. Não divida Sum por Count com inteiros.
  • Min/Max dão o valor; MinBy/MaxBy dão o elemento.
  • Aggregate faz qualquer acúmulo; prefira a versão com semente.
  • Any/All respondem sim ou não sobre a lista toda.
  • Lista vazia: Count e Sum dão 0; Average, Min e Max de números lançam InvalidOperationException ("Sequence contains no elements"). Use Any() antes ou a versão anulável.

Próxima aula: Juntar (Join), para combinar duas listas, como produtos e vendas.