C# IComparable e IComparer
Ordenar números e nomes é fácil: lista.Sort() e pronto. Mas a professora Ana tem uma lista de alunos, cada um com nome e nota, e quer o ranking da turma. Maior nota primeiro? Ordem alfabética? O C# não tem como adivinhar. Nesta aula você vai ensinar o C# a comparar dois objetos seus, de dois jeitos: com IComparable<T> (a ordem "natural" da classe) e com IComparer<T> (quantas ordens diferentes você quiser).
- O que o
CompareToresponde: negativo, zero ou positivo. - O erro real de ordenar objetos que não sabem se comparar.
- Implementar
IComparable<T>: a ordem natural de uma classe. - Criar comparadores com
IComparer<T>, com lambda e comComparer<T>.Create. - Ordem decrescente e desempate (por nota e, empatou, por nome).
- Textos: a ordem da cultura pt-BR ×
StringComparer.Ordinal. - A armadilha do
SortedSet: "empate" vira "repetido".
Antes, é bom conhecer Interfaces, List e Lambdas. Todos os programas desta aula rodaram com o .NET 10 na máquina da Ana (usuária ana, computador notebook), na pasta ~/cs, com dotnet run arquivo.cs.
A ideia: só uma pergunta
Para pôr uma fila de pessoas em ordem de altura, você não precisa saber a altura exata de ninguém. Basta, repetidas vezes, colocar duas pessoas lado a lado e perguntar: "quem vem antes?". Os métodos de ordenação fazem isso: comparam pares de itens, muitas vezes, até tudo estar no lugar.
No C#, a resposta a "quem vem antes?" é um número inteiro:
a.CompareTo(b) devolve… | Quer dizer |
|---|---|
| um número negativo (como -1) | a vem antes de b |
| zero | empate: tanto faz a ordem |
| um número positivo (como 1) | a vem depois de b |
Um truque para lembrar: pense em a - b. Se a é menor, a conta dá negativo; se são iguais, zero; se a é maior, positivo. Só o sinal importa, não o valor: -1 e -57 querem dizer a mesma coisa.
1. Os tipos que já sabem se comparar
List<int> notas = [7, 10, 5, 8];
notas.Sort();
Console.WriteLine(string.Join(", ", notas));
List<string> nomes = ["Carla", "ana", "Bruno", "Ágata", "Zé"];
nomes.Sort();
Console.WriteLine(string.Join(", ", nomes));
Console.WriteLine(5.CompareTo(8));
Console.WriteLine(8.CompareTo(8));
Console.WriteLine(8.CompareTo(5));
Console.WriteLine("Ana".CompareTo("Bruno"));
$ dotnet run prontos.cs 5, 7, 8, 10 Ágata, ana, Bruno, Carla, Zé « ordem de dicionário do português -1 « 5 vem antes de 8 0 « empate 1 « 8 vem depois de 5 -1 « "Ana" vem antes de "Bruno"
int, double, decimal, string, DateTime e muitos outros já implementam a interface IComparable<T>, que tem um único método, o CompareTo ("comparar com"). É ele que o Sort chama por baixo. Repare que os textos saíram na ordem de um dicionário de papel: o "Ágata" com acento ficou junto do "ana", e o "ana" minúsculo não foi para o fim. Isso é a cultura pt-BR trabalhando (mais na seção 4).
2. Objetos seus: o erro e o IComparable<T>
Com uma classe sua, o Sort não sabe o que fazer:
List<Aluno> turma =
[
new Aluno("Carla", 8.5),
new Aluno("Ana", 9.0),
new Aluno("Bruno", 7.0),
];
turma.Sort();
record Aluno(string Nome, double Nota);
$ dotnet run falha.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.Collections.Generic.ArraySortHelper`1.SwapIfGreater(Span`1 keys, Comparison`1 comparer, Int32 i, Int32 j) 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, IComparer`1 comparer) --- End of inner exception stack trace --- at System.Collections.Generic.ArraySortHelper`1.Sort(Span`1 keys, IComparer`1 comparer) at System.Collections.Generic.List`1.Sort(Int32 index, Int32 count, IComparer`1 comparer) at Program.<Main>$(String[] args) in /home/ana/cs/falha.cs:line 7 $ echo $? 134
- InvalidOperationException: Failed to compare two elements in the array.
- "Falha ao comparar dois elementos do array." O
Sorttentou perguntar "quem vem antes?" e não conseguiu. - ---> System.ArgumentException: At least one object must implement IComparable.
- A causa (a "exceção interna", marcada com
--->): "pelo menos um dos objetos tem de implementarIComparable". Nenhum aluno sabe se comparar. - falha.cs:line 7
- A linha do
turma.Sort();. O programa compilou sem reclamar: o problema só aparece quando ele roda. - record Aluno(…)
- Um record sabe dizer se dois alunos são iguais (
Equals), mas não qual vem antes. Com umaclasso erro seria o mesmo.
A primeira solução é dar à classe uma ordem natural, implementando IComparable<Aluno>. Para a professora, a ordem natural de alunos é pela nota:
List<Aluno> turma =
[
new Aluno("Carla", 8.5),
new Aluno("Ana", 9.0),
new Aluno("Bruno", 7.0),
];
turma.Sort();
foreach (Aluno a in turma)
Console.WriteLine(a);
Console.WriteLine($"Maior nota: {turma.Max()}");
Console.WriteLine(turma[0].CompareTo(turma[1]));
class Aluno : IComparable<Aluno>
{
public string Nome { get; }
public double Nota { get; }
public Aluno(string nome, double nota)
{
Nome = nome;
Nota = nota;
}
public int CompareTo(Aluno? outro)
{
if (outro is null) return 1;
return Nota.CompareTo(outro.Nota);
}
public override string ToString() => $"{Nome} ({Nota})";
}
$ dotnet run icomparable.cs Bruno (7) Carla (8,5) Ana (9) Maior nota: Ana (9) « o Max do LINQ também usa o CompareTo -1 « Bruno (7) vem antes de Carla (8,5)
- class Aluno : IComparable<Aluno>
- "Um aluno sabe se comparar com outro aluno." A interface exige um método:
int CompareTo(Aluno? outro). - Aluno? outro
- O
?diz queoutropode sernull: é assim que a interface está escrita no .NET com as referências anuláveis ligadas. - if (outro is null) return 1;
- A convenção do .NET: qualquer objeto vem depois de
null(onullfica no começo). - return Nota.CompareTo(outro.Nota);
- O coração do método. Em vez de fazer contas, repasse a pergunta para um tipo que já sabe responder: o
doublecompara as notas e devolve o sinal certo. - public override string ToString()
- Só para o
Console.WriteLine(a)mostrar algo legível (veja A Classe object).
Com o IComparable, o aluno passou a funcionar em tudo que ordena ou procura o maior: Sort, Max, Min, Order(), SortedSet, SortedDictionary…
Missão: rode e veja a InvalidOperationException. Faça a classe Produto implementar IComparable<Produto>, comparando pelo Preco, para que o Sort ponha os produtos do mais barato para o mais caro.
Pão: R$ 0,75 Leite: R$ 5,49 Café: R$ 18,90
List<Produto> produtos =
[
new Produto("Café", 18.90m),
new Produto("Pão", 0.75m),
new Produto("Leite", 5.49m),
];
produtos.Sort();
foreach (Produto p in produtos)
Console.WriteLine($"{p.Nome}: {p.Preco:C}");
class Produto
{
public string Nome { get; }
public decimal Preco { get; }
public Produto(string nome, decimal preco)
{
Nome = nome;
Preco = preco;
}
}
List<Produto> produtos =
[
new Produto("Café", 18.90m),
new Produto("Pão", 0.75m),
new Produto("Leite", 5.49m),
];
produtos.Sort();
foreach (Produto p in produtos)
Console.WriteLine($"{p.Nome}: {p.Preco:C}");
class Produto : IComparable<Produto>
{
public string Nome { get; }
public decimal Preco { get; }
public Produto(string nome, decimal preco)
{
Nome = nome;
Preco = preco;
}
public int CompareTo(Produto? outro)
{
if (outro is null) return 1;
return Preco.CompareTo(outro.Preco);
}
}
3. Outras ordens: IComparer<T>
Uma classe só tem uma ordem natural. Mas a professora quer a lista de chamada por nome e o ranking pela maior nota. Para cada ordem extra, você cria um comparador: um objeto separado, que sabe comparar dois alunos de um jeito. Ele implementa IComparer<T> ("comparador de T"), que tem o método Compare(x, y), com o mesmo significado do sinal.
List<Aluno> turma =
[
new Aluno("Carla", 8.5),
new Aluno("Ana", 9.0),
new Aluno("Bruno", 7.0),
new Aluno("Davi", 9.0),
];
turma.Sort(new PorNome());
Console.WriteLine(string.Join(", ", turma));
turma.Sort((a, b) => b.Nota.CompareTo(a.Nota));
Console.WriteLine(string.Join(", ", turma));
var porNotaDepoisNome = Comparer<Aluno>.Create((a, b) =>
{
int r = b.Nota.CompareTo(a.Nota);
if (r != 0) return r;
return a.Nome.CompareTo(b.Nome);
});
turma.Sort(porNotaDepoisNome);
Console.WriteLine(string.Join(", ", turma));
class PorNome : IComparer<Aluno>
{
public int Compare(Aluno? x, Aluno? y) => string.Compare(x?.Nome, y?.Nome);
}
record Aluno(string Nome, double Nota)
{
public override string ToString() => $"{Nome} {Nota}";
}
$ dotnet run icomparer.cs Ana 9, Bruno 7, Carla 8,5, Davi 9 « PorNome: A → Z Ana 9, Davi 9, Carla 8,5, Bruno 7 « maior nota primeiro Ana 9, Davi 9, Carla 8,5, Bruno 7 « maior nota; empate, por nome
- class PorNome : IComparer<Aluno>
- Um comparador: uma classe pequena, só com o método
Compare. OAlunonem precisa saber que ele existe. - string.Compare(x?.Nome, y?.Nome)
- Repassa a pergunta para a comparação de textos (na cultura atual, pt-BR). O
?.evita erro se algum aluno fornull(veja Operadores ??, ?. e ??=). - turma.Sort(new PorNome())
- Ordena usando o comparador, em vez da ordem natural.
- turma.Sort((a, b) => b.Nota.CompareTo(a.Nota))
- O atalho: uma lambda que recebe dois alunos e devolve o sinal. Repare na troca:
bantes dea. Isso inverte a resposta e dá a ordem decrescente (maior nota primeiro). - Comparer<Aluno>.Create(…)
- Transforma uma lambda num
IComparer<Aluno>de verdade, que você pode guardar numa variável e passar para umSortedSet, por exemplo. - if (r != 0) return r; return a.Nome.CompareTo(b.Nome);
- O desempate: compare pela nota; se deu zero (empate), compare pelo nome.
Na segunda linha, a Ana saiu antes do Davi (os dois com 9), mas o Sort da List não promete manter a ordem dos empatados: ele pode trocá-los, dependendo dos dados. Se a ordem dos empates importa, escreva o desempate, como na terceira ordenação. (O OrderBy do LINQ é "estável": ele mantém os empatados na ordem em que estavam.)
IComparable<T> | IComparer<T> | |
|---|---|---|
| Quem implementa | A própria classe (class Aluno : IComparable<Aluno>) | Uma classe separada, ou uma lambda com Comparer<T>.Create |
| Método | a.CompareTo(b) | comparador.Compare(a, b) |
| Quantas ordens | Uma: a natural | Quantas você quiser |
| Uso | lista.Sort() | lista.Sort(comparador) ou lista.Sort((a, b) => …) |
| Classe que não é sua | Não dá para mudar | Funciona: o comparador fica de fora |
4. Comparar textos: cultura × Ordinal
Comparar textos tem uma pegadinha. A comparação padrão segue as regras da língua (a cultura pt-BR na máquina da Ana): acentos e maiúsculas pesam pouco, como num dicionário. Já o StringComparer.Ordinal compara o código de cada letra, um por um:
List<string> nomes = ["Carla", "ana", "Bruno", "Ágata", "Zé"];
nomes.Sort(StringComparer.Ordinal);
Console.WriteLine(string.Join(", ", nomes));
$ dotnet run ordinal.cs Bruno, Carla, Zé, ana, Ágata
No código das letras, todas as maiúsculas sem acento (A–Z) vêm antes das minúsculas (a–z), e as letras acentuadas vêm depois de todas elas. Por isso "ana" e "Ágata" foram para o fim. Para mostrar uma lista a uma pessoa, use a ordem da cultura (o padrão do Sort()). O Ordinal é mais rápido e não muda de máquina para máquina, por isso serve para comparações "técnicas": nomes de arquivos, códigos, chaves.
5. A armadilha: empate num SortedSet
Coleções ordenadas como SortedSet<T> e SortedDictionary<TKey, TValue> usam o comparador também para saber se dois itens são o mesmo. Para elas, CompareTo igual a zero quer dizer "repetido":
var porNota = Comparer<Aluno>.Create((a, b) => a.Nota.CompareTo(b.Nota));
var ranking = new SortedSet<Aluno>(porNota);
Console.WriteLine(ranking.Add(new Aluno("Ana", 9.0)));
Console.WriteLine(ranking.Add(new Aluno("Bruno", 7.0)));
Console.WriteLine(ranking.Add(new Aluno("Davi", 9.0)));
Console.WriteLine(string.Join(", ", ranking));
record Aluno(string Nome, double Nota);
$ dotnet run sortedset.cs
True
True
False « o Davi foi recusado!
Aluno { Nome = Bruno, Nota = 7 }, Aluno { Nome = Ana, Nota = 9 }
O Davi tem nota 9, como a Ana. O comparador disse "empate" (zero), e o SortedSet entendeu "já tenho esse aluno". O Add devolveu false e o Davi sumiu, sem erro nenhum. O conserto é o mesmo desempate da seção 3: se a nota empatar, compare pelo nome. Assim, só dá zero quando o aluno é o mesmo de verdade.
Quando dá errado
InvalidOperationException: Failed to compare two elements in the array. → At least one object must implement IComparable.
O que significa: você chamou Sort(), Max(), Order() ou criou um SortedSet de objetos que não sabem se comparar (seção 2).
Como consertar: implemente IComparable<T> na classe, ou passe um comparador: lista.Sort((a, b) => a.Nota.CompareTo(b.Nota)).
error CS0535: "Aluno" não implementa membro de interface "IComparable<Aluno>.CompareTo(Aluno?)"
O que significa: a classe diz : IComparable<Aluno>, mas não tem o método CompareTo (ou ele tem outro nome ou outro tipo de parâmetro).
$ dotnet run semmetodo.cs /home/ana/cs/semmetodo.cs(1,15): error CS0535: "Aluno" não implementa membro de interface "IComparable<Aluno>.CompareTo(Aluno?)" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Como consertar: escreva public int CompareTo(Aluno? outro). A mensagem mostra a assinatura exata que falta.
warning CS8767: A nulidade de tipos de referência no tipo de parâmetro 'outro' … não corresponde ao membro implementado implicitamente 'int IComparable<Aluno>.CompareTo(Aluno? other)'
O que significa: você escreveu CompareTo(Aluno outro), sem o ?. A interface diz que o parâmetro pode ser null; o seu método diz que não pode. Compila, mas com aviso.
Como consertar: use Aluno? outro e trate o null na primeira linha: if (outro is null) return 1;.
O que significa: o sinal está invertido. a.X.CompareTo(b.X) dá a ordem crescente; b.X.CompareTo(a.X), a decrescente.
Como consertar: troque a e b de lugar (ou chame lista.Reverse() depois do Sort).
SortedSet (ou do SortedDictionary)
O que significa: o comparador deu zero para dois itens diferentes, e a coleção os tratou como repetidos (seção 5).
Como consertar: acrescente um desempate que só dá zero para o mesmo item.
Referência rápida
| Escrita | O que faz |
|---|---|
a.CompareTo(b) | Negativo: a antes; zero: empate; positivo: a depois |
class T : IComparable<T> + public int CompareTo(T? outro) | Ordem natural da classe |
return Campo.CompareTo(outro.Campo); | Repassa a comparação para um tipo pronto |
class C : IComparer<T> + public int Compare(T? x, T? y) | Comparador separado |
Comparer<T>.Create((a, b) => …) | Comparador a partir de uma lambda |
lista.Sort() / lista.Sort(comparador) / lista.Sort((a, b) => …) | Ordena: natural / com comparador / com lambda |
(a, b) => b.X.CompareTo(a.X) | Ordem decrescente |
int r = …; if (r != 0) return r; return …; | Desempate por um segundo critério |
StringComparer.Ordinal / OrdinalIgnoreCase | Compara textos pelo código das letras |
new SortedSet<T>(comparador) | Conjunto ordenado; Compare zero = repetido |
Exercícios
1. A interface da ordem natural
Complete para os produtos terem uma ordem natural:
2. Repassar a pergunta
Complete o CompareTo que compara pelo preço:
3. a.CompareTo(b) devolveu -12. O que isso quer dizer?
4. Qual lambda ordena os alunos da maior nota para a menor?
5. Você precisa ordenar objetos de uma classe de uma biblioteca, que você não pode mudar. O que usa?
Missão final: o ranking da turma
Missão: o ranking está em ordem de nome. Troque o Sort por um com lambda (ou com Comparer<Aluno>.Create) que ponha a maior nota primeiro e, quando a nota empatar, a ordem alfabética do nome. Repare que o Davi vem antes da Ana na lista: sem o desempate, ele fica na frente dela.
1º Ana 9 2º Davi 9 3º Carla 8,5 4º Bruno 7
List<Aluno> turma =
[
new Aluno("Davi", 9.0),
new Aluno("Bruno", 7.0),
new Aluno("Ana", 9.0),
new Aluno("Carla", 8.5),
];
turma.Sort((a, b) => a.Nome.CompareTo(b.Nome));
for (int i = 0; i < turma.Count; i++)
Console.WriteLine($"{i + 1}º {turma[i].Nome} {turma[i].Nota}");
record Aluno(string Nome, double Nota);
List<Aluno> turma =
[
new Aluno("Davi", 9.0),
new Aluno("Bruno", 7.0),
new Aluno("Ana", 9.0),
new Aluno("Carla", 8.5),
];
turma.Sort((a, b) =>
{
int r = b.Nota.CompareTo(a.Nota);
if (r != 0) return r;
return a.Nome.CompareTo(b.Nome);
});
for (int i = 0; i < turma.Count; i++)
Console.WriteLine($"{i + 1}º {turma[i].Nome} {turma[i].Nota}");
record Aluno(string Nome, double Nota);
Resumo
- Ordenar é responder muitas vezes "quem vem antes?". A resposta é um
int: negativo (antes), zero (empate), positivo (depois). - Ordenar objetos que não sabem se comparar dá
InvalidOperationException: Failed to compare two elements in the array. IComparable<T>dá à classe uma ordem natural (CompareTo); repasse a pergunta para um campo:Nota.CompareTo(outro.Nota).IComparer<T>,Comparer<T>.Createou uma lambda noSortdão outras ordens, sem mexer na classe. Trocaraebinverte a ordem.- Escreva o desempate: o
Sortnão garante a ordem dos empatados, e umSortedSetdescarta itens com comparação zero. - Textos: a comparação padrão segue a cultura (pt-BR);
StringComparer.Ordinalsegue o código das letras.