C# LINQ OrderBy e ThenBy
Um relatório só é útil se estiver em ordem: do mais barato ao mais caro, por categoria, em ordem alfabética. Com o LINQ, você diz por qual informação ordenar (a chave) e ele ordena: OrderBy(p => p.Preco). Para desempatar ("por categoria e, dentro de cada uma, por preço"), entra o ThenBy. Esta aula também mostra a armadilha mais comum (dois OrderBy seguidos) e como os textos em português são ordenados.
- Ordenar com
OrderBy(crescente) eOrderByDescending(decrescente). - Desempatar com
ThenByeThenByDescending. - Por que dois
OrderByseguidos não fazem o que parece. - O que é uma ordenação estável.
Order()eOrderDescending()para listas de números e textos.- Como acentos e maiúsculas são ordenados, e o
StringComparer.Ordinal. - A diferença entre
OrderBye oSortdaList. - Os erros reais:
CS1061(ThenBysemOrderBy) e aInvalidOperationExceptionde uma chave que não sabe se comparar.
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: a fila da chamada
Uma professora pede: "façam uma fila por altura". Ela não diz como trocar as pessoas de lugar; diz só o critério, a altura. Se dois alunos têm a mesma altura, ela pode completar: "e, entre os da mesma altura, por ordem alfabética". O primeiro critério é o OrderBy; o desempate, o ThenBy.
No LINQ, o critério é uma lambda que, dado um produto, devolve a chave da ordenação: p => p.Preco quer dizer "ordene pelo preço".
1. OrderBy e OrderByDescending
// (a lista de produtos)
var maisBaratos = produtos.OrderBy(p => p.Preco);
var maisCaros = produtos.OrderByDescending(p => p.Preco);
var porNome = produtos.OrderBy(p => p.Nome);
Console.WriteLine(string.Join(", ", maisBaratos.Select(p => $"{p.Nome} {p.Preco}")));
Console.WriteLine(string.Join(", ", maisCaros.Select(p => $"{p.Nome} {p.Preco}")));
Console.WriteLine(string.Join(", ", porNome.Select(p => p.Nome)));
Console.WriteLine(string.Join(", ", produtos.Select(p => p.Nome)));
// (o record Produto)
$ dotnet run orderby.cs Borracha 1,75, Lápis 2,50, Caderno 18,90, Caneca 22,50, Estojo 24,90, Garrafa 35,00, Mochila 129,90 Mochila 129,90, Garrafa 35,00, Estojo 24,90, Caneca 22,50, Caderno 18,90, Lápis 2,50, Borracha 1,75 Borracha, Caderno, Caneca, Estojo, Garrafa, Lápis, Mochila Caderno, Lápis, Mochila, Borracha, Estojo, Garrafa, Caneca « a lista original não mudou
- OrderBy
- "Ordenar por": do menor para o maior (crescente). Para números, do menor valor; para textos, em ordem alfabética.
- OrderByDescending
- "Ordenar por, decrescente" (descending = "descendo"): do maior para o menor.
- p => p.Preco
- A chave: o valor pelo qual cada produto é comparado. Pode ser qualquer coisa comparável: número, texto, data, ou uma conta (
p => p.Preco * p.Estoque).
A última linha mostra que produtos continua na ordem em que foi escrita: o OrderBy produz uma sequência nova, como todo o LINQ. E, como o Where e o Select, ele é adiado: a ordenação só acontece quando o resultado é percorrido.
Missão: a Ana quer ver primeiro os produtos com mais unidades no estoque. Troque a ordenação por nome por uma ordenação decrescente pelo estoque. A primeira linha deve ser Lápis 40 e a última, Estojo 0.
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 ordenados = produtos.OrderBy(p => p.Nome);
foreach (Produto p in ordenados)
{
Console.WriteLine($"{p.Nome} {p.Estoque}");
}
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 ordenados = produtos.OrderByDescending(p => p.Estoque);
foreach (Produto p in ordenados)
{
Console.WriteLine($"{p.Nome} {p.Estoque}");
}
record Produto(string Nome, string Categoria, decimal Preco, int Estoque);
2. Desempatar com ThenBy
O relatório da Ana agrupa visualmente por categoria e, dentro de cada categoria, mostra do mais caro ao mais barato:
// (a lista de produtos)
var relatorio = produtos
.OrderBy(p => p.Categoria)
.ThenByDescending(p => p.Preco);
foreach (Produto p in relatorio)
{
Console.WriteLine($"{p.Categoria,-10} {p.Nome,-9} {p.Preco,10:C}");
}
Console.WriteLine("--- sintaxe de consulta");
var consulta =
from p in produtos
orderby p.Categoria, p.Preco descending
select p.Nome;
Console.WriteLine(string.Join(", ", consulta));
// (o record Produto)
$ dotnet run thenby.cs Bolsas Mochila R$ 129,90 Bolsas Estojo R$ 24,90 Cozinha Garrafa R$ 35,00 Cozinha Caneca R$ 22,50 Papelaria Caderno R$ 18,90 Papelaria Lápis R$ 2,50 Papelaria Borracha R$ 1,75 --- sintaxe de consulta Mochila, Estojo, Garrafa, Caneca, Caderno, Lápis, Borracha
| Linha | O que faz |
|---|---|
.OrderBy(p => p.Categoria) | O critério principal: categorias em ordem alfabética (Bolsas, Cozinha, Papelaria). |
.ThenByDescending(p => p.Preco) | "Depois por", decrescente: só desempata produtos da mesma categoria. Dentro de Papelaria: Caderno (18,90), Lápis (2,50), Borracha (1,75). |
{p.Categoria,-10} | Alinha o texto à esquerda numa coluna de 10 caracteres (veja Formatar Números). |
orderby p.Categoria, p.Preco descending | A mesma ordenação na sintaxe de consulta: os critérios separados por vírgula, e descending depois do que é decrescente. |
Dá para encadear quantos ThenBy quiser: .OrderBy(…).ThenBy(…).ThenByDescending(…). Cada um só entra em ação quando todos os anteriores empataram.
A armadilha: dois OrderBy seguidos
Parece natural escrever o desempate com outro OrderBy. Veja o que sai:
// (a lista de produtos)
var errado = produtos
.OrderBy(p => p.Categoria)
.OrderByDescending(p => p.Preco);
Console.WriteLine(string.Join(", ", errado.Select(p => $"{p.Categoria}/{p.Nome}")));
// (o record Produto)
$ dotnet run duasorderby.cs Bolsas/Mochila, Cozinha/Garrafa, Bolsas/Estojo, Cozinha/Caneca, Papelaria/Caderno, Papelaria/Lápis, Papelaria/Borracha
As categorias ficaram misturadas: a lista saiu ordenada só pelo preço. O segundo OrderBy reordena tudo de novo, do zero, e a ordem por categoria se perde. Compila, roda, não dá erro nenhum, e o relatório sai errado. Regra: um OrderBy (ou OrderByDescending) no começo, e todos os desempates com ThenBy.
Ordem estável: o empate mantém a ordem original
// (a lista de produtos)
var porCategoria = produtos.OrderBy(p => p.Categoria);
Console.WriteLine(string.Join(", ", porCategoria.Select(p => $"{p.Categoria}/{p.Nome}")));
// (o record Produto)
$ dotnet run estavel.cs Bolsas/Mochila, Bolsas/Estojo, Cozinha/Garrafa, Cozinha/Caneca, Papelaria/Caderno, Papelaria/Lápis, Papelaria/Borracha
Sem ThenBy, os produtos da mesma categoria ficaram na ordem em que estavam na lista (Caderno, Lápis, Borracha). O OrderBy do LINQ é estável (stable): empates nunca trocam de lugar. Isso é garantido, e você pode contar com isso.
3. Listas simples: Order() .NET 7
Quando os próprios elementos já são a chave (uma lista de preços, de nomes), não precisa de lambda: Order() e OrderDescending() ordenam pelos próprios valores. E aqui aparece um detalhe importante sobre textos:
decimal[] precos = [18.90m, 2.50m, 129.90m, 1.75m];
string[] nomes = ["lápis", "Caderno", "água", "Borracha", "estojo", "Álbum"];
Console.WriteLine(string.Join(" | ", precos.Order()));
Console.WriteLine(string.Join(" | ", precos.OrderDescending()));
Console.WriteLine(string.Join(" | ", nomes.Order()));
Console.WriteLine(string.Join(" | ", nomes.Order(StringComparer.Ordinal)));
$ dotnet run order.cs 1,75 | 2,50 | 18,90 | 129,90 129,90 | 18,90 | 2,50 | 1,75 água | Álbum | Borracha | Caderno | estojo | lápis Borracha | Caderno | estojo | lápis | Álbum | água
- Terceira linha: por padrão, textos são ordenados pelas regras do idioma do computador (a cultura pt-BR da Ana, veja Cultura), como num dicionário:
águaeÁlbumficam no "a", e maiúsculas e minúsculas se misturam (Cadernoantes deestojo). É o que uma pessoa espera ver. - Quarta linha: com
StringComparer.Ordinal("pela ordem dos códigos"), a comparação é pelos números de cada caractere na tabela Unicode: primeiro todas as maiúsculas sem acento, depois as minúsculas, e as letras acentuadas por último. É rápido e igual em qualquer computador, mas não serve para mostrar a uma pessoa.
Use a ordem padrão (do idioma) para o que vai aparecer na tela, e StringComparer.Ordinal para coisas técnicas, como códigos de produto. O OrderBy também aceita o comparador: produtos.OrderBy(p => p.Nome, StringComparer.Ordinal).
4. OrderBy × List.Sort
Na aula de Lambdas, a lista foi ordenada com Sort. A diferença:
// (a lista de produtos)
var ordenada = produtos.OrderBy(p => p.Preco).ToList();
Console.WriteLine($"Original: {produtos[0].Nome}, nova: {ordenada[0].Nome}");
produtos.Sort((a, b) => a.Preco.CompareTo(b.Preco));
Console.WriteLine($"Depois do Sort: {produtos[0].Nome}");
var invertida = produtos.AsEnumerable().Reverse();
Console.WriteLine(string.Join(", ", invertida.Select(p => p.Nome)));
// (o record Produto)
$ dotnet run sort.cs Original: Caderno, nova: Borracha Depois do Sort: Borracha Mochila, Garrafa, Estojo, Caneca, Caderno, Lápis, Borracha
lista.OrderBy(p => chave) | lista.Sort((a, b) => …) | |
|---|---|---|
| A lista original | fica igual | muda (é reordenada) |
| Devolve | uma sequência nova (adiada) | nada (void) |
| Funciona em | qualquer IEnumerable<T> (arrays, resultados de outro LINQ…) | só List<T> (e arrays, com Array.Sort) |
| Empates | mantêm a ordem (estável) | podem trocar de lugar (não estável) |
| Desempate | ThenBy | dentro da própria lambda |
O Reverse() ("inverter") vira a sequência de trás para frente, sem comparar nada. O AsEnumerable() antes dele garante que é o Reverse do LINQ, que devolve uma sequência nova; a List tem um Reverse() próprio, que inverte a lista no lugar e não devolve nada.
Quando dá errado
ThenBy sem OrderBy antes
Este código tem um erro de propósito.
// (a lista de produtos, linhas 1 a 10) var lista = produtos.ThenBy(p => p.Nome); // (o record Produto)
$ dotnet run thenby-erro.cs /home/ana/cs/thenby-erro.cs(12,22): error CS1061: ‘List<Produto>’ não contém uma definição para "ThenBy" e não foi possível encontrar nenhum método de extensão "ThenBy" que aceite um primeiro argumento do tipo ‘List<Produto>’ (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: o ThenBy só existe depois de um OrderBy. Ele é uma extensão de IOrderedEnumerable<T> ("enumerável já ordenado"), o tipo que o OrderBy devolve; uma lista comum não tem critério principal para desempatar.
Como consertar: comece com OrderBy: produtos.OrderBy(p => p.Categoria).ThenBy(p => p.Nome).
Este código tem um erro de propósito.
// (a lista de produtos, linhas 1 a 10)
var ordenados = produtos.OrderBy(p => p);
foreach (Produto p in ordenados)
{
Console.WriteLine(p.Nome);
}
// (o record Produto)
$ dotnet run comparar-erro.cs Unhandled exception. System.InvalidOperationException: Failed to compare two elements in the array. ---> System.ArgumentException: At least one object must implement IComparable. at System.Collections.Comparer.Compare(Object a, Object b) at System.Linq.Enumerable.EnumerableSorter`2.CompareAnyKeys(Int32 index1, Int32 index2) at System.Collections.Generic.ArraySortHelper`1.InsertionSort(Span`1 keys, Comparison`1 comparer) at System.Collections.Generic.ArraySortHelper`1.IntroSort(Span`1 keys, Int32 depthLimit, Comparison`1 comparer) at System.Collections.Generic.ArraySortHelper`1.Sort(Span`1 keys, Comparison`1 comparer) --- End of inner exception stack trace --- at System.Collections.Generic.ArraySortHelper`1.Sort(Span`1 keys, Comparison`1 comparer) at System.MemoryExtensions.Sort[T](Span`1 span, Comparison`1 comparison) at System.Linq.Enumerable.EnumerableSorter`2.QuickSort(Int32[] keys, Int32 lo, Int32 hi) at System.Linq.Enumerable.OrderedIterator`2.MoveNext() at Program.<Main>$(String[] args) in /home/ana/cs/comparar-erro.cs:line 13
O que quer dizer: "falha ao comparar dois elementos", causada por (--->) "pelo menos um objeto precisa implementar IComparable". A chave é o produto inteiro, e o C# não sabe se um Produto vem antes ou depois de outro: pelo nome? pelo preço? As linhas do meio são o caminho dentro do .NET; a que importa é a última, a linha 13 do seu programa (o foreach, onde a ordenação adiada finalmente rodou). Repare que o erro só aparece ao rodar: o compilador aceitou.
Como consertar: ordene por uma propriedade comparável, OrderBy(p => p.Nome), ou ensine o tipo a se comparar implementando IComparable<Produto> (veja IComparable e IComparer).
OrderBy: nenhum erro, resultado errado
Visto na seção 2: o segundo OrderBy desfaz o primeiro. Como consertar: troque o segundo por ThenBy (ou ThenByDescending).
Referência
| Código | O que faz |
|---|---|
OrderBy(p => chave) | Ordena crescente pela chave (estável, adiado). |
OrderByDescending(p => chave) | Ordena decrescente. |
.ThenBy(p => chave) | Desempate crescente (só depois de um OrderBy). |
.ThenByDescending(p => chave) | Desempate decrescente. |
Order() / OrderDescending() | Ordena pelos próprios elementos .NET 7. |
OrderBy(p => p.Nome, StringComparer.Ordinal) | Ordena textos pelo código dos caracteres, sem regras de idioma. |
Reverse() | Inverte a sequência. |
orderby a, b descending | Sintaxe de consulta: OrderBy(a).ThenByDescending(b). |
Exercícios
1. Do mais caro ao mais barato
Complete para ordenar os produtos do mais caro para o mais barato:
2. O desempate
Complete para ordenar por categoria e, dentro de cada uma, por nome:
3. Na sintaxe de consulta
Complete a cláusula para ordenar pelo estoque, do maior para o menor:
4. produtos.OrderBy(p => p.Categoria).OrderBy(p => p.Nome) ordena…
5. Dois produtos têm o mesmo preço. Depois de OrderBy(p => p.Preco), em que ordem eles ficam?
6. Depois de var o = produtos.OrderBy(p => p.Preco);, como fica a lista produtos?
Missão final: a vitrine deve mostrar os produtos por categoria e, dentro de cada categoria, em ordem alfabética. O código caiu na armadilha dos dois OrderBy (rode e veja as categorias misturadas). Conserte. A saída deve começar com Bolsas/Estojo e Bolsas/Mochila e terminar com Papelaria/Lápis.
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 vitrine = produtos
.OrderBy(p => p.Categoria)
.OrderBy(p => p.Nome);
foreach (Produto p in vitrine)
{
Console.WriteLine($"{p.Categoria}/{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 vitrine = produtos
.OrderBy(p => p.Categoria)
.ThenBy(p => p.Nome);
foreach (Produto p in vitrine)
{
Console.WriteLine($"{p.Categoria}/{p.Nome}");
}
record Produto(string Nome, string Categoria, decimal Preco, int Estoque);
Resumo
OrderBy(p => chave)ordena do menor para o maior;OrderByDescending, do maior para o menor. A lista original não muda.- Desempates vêm com
ThenBy/ThenByDescending, que só existem depois de umOrderBy(CS1061). - Dois
OrderByseguidos: o segundo refaz tudo, sem erro nenhum. Use umOrderBye depoisThenBy. - A ordenação é estável: empates mantêm a ordem original.
Order()ordena listas simples. Textos seguem as regras do idioma (acentos e maiúsculas juntos);StringComparer.Ordinalcompara pelos códigos.OrderBycria uma sequência nova e adiada;List.Sortreordena a própria lista.- A chave precisa ser comparável; senão,
InvalidOperationExceptionao rodar.
Próxima aula: Agrupar (GroupBy).