1. Ordenação multi-nível (hierárquica)
Por que comparar nem sempre é simples?
Em entrevistas de C# sempre rola pergunta sobre ordenação e comparação de objetos complexos. Não é só moda, é porque isso dá problema de verdade pra qualquer dev. Buscar registros no banco, ordenar listas e tabelas na interface, garantir unicidade em coleções — tudo isso depende de comparar objetos do jeito certo.
Comparar números é fácil. Mas se a gente quer ordenar usuários por sobrenome, depois por nome, e ainda tem campo que pode estar vazio (null) ou strings em idiomas diferentes... aí já precisa de um esquema mais organizado.
Pedido comum: primeiro ordena por um critério, se empatar — por outro, se empatar de novo — por um terceiro.
Exemplo: Usuário com nome, sobrenome e data de nascimento
public class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
// Só pra ficar bonito — mostra info do usuário
public override string ToString()
=> $"{LastName} {FirstName} ({BirthDate:yyyy-MM-dd})";
}
Por que precisa de hierarquia?
Imagina que temos uma lista de usuários e queremos mostrar em ordem alfabética: primeiro pelo sobrenome, depois pelo nome. Se nome e sobrenome forem iguais — pela data de nascimento.
Lógica de comparação: "corrente de responsabilidade"
É tipo como ordenam participantes de olimpíada: primeiro pela pontuação, se empatar — pelo tempo, se empatar de novo — pelo nome. No código, fica assim:
public class UserComparer : IComparer<User>
{
public int Compare(User x, User y)
{
// Compara sobrenomes
int result = string.Compare(x.LastName, y.LastName, StringComparison.OrdinalIgnoreCase);
if (result != 0) return result; // Se diferente — já era, retorna
// Se sobrenome igual — compara nomes
result = string.Compare(x.FirstName, y.FirstName, StringComparison.OrdinalIgnoreCase);
if (result != 0) return result;
// Se nome e sobrenome iguais — compara datas de nascimento
return x.BirthDate.CompareTo(y.BirthDate);
}
}
Como usar:
var users = new List<User>
{
new User { FirstName = "Ivan", LastName = "Ivanov", BirthDate = new DateTime(1990, 1, 1) },
new User { FirstName = "Petr", LastName = "Ivanov", BirthDate = new DateTime(1992, 5, 1) },
new User { FirstName = "Anna", LastName = "Petrova", BirthDate = new DateTime(1985, 8, 30) }
};
users.Sort(new UserComparer());
users.ForEach(Console.WriteLine);
// Petrova Anna (1985-08-30)
// Ivanov Ivan (1990-01-01)
// Ivanov Petr (1992-05-01)
2. Como comparar strings? Particularidades culturais
Comparação de strings: Ordinal, CurrentCulture, InvariantCulture
String parece igual em qualquer lugar, mas não é bem assim! Tipo, o russo ё e е, alemão ss e ß, diferença de maiúscula/minúscula...
No .NET, pra comparar strings tem regras especiais, usando StringComparison. Isso muda a ordenação e até a busca.
Exemplo de comparação de strings:
// em alemão ß é quase igual a ss
string a = "straße";
string b = "STRASSE";
bool eq1 = string.Equals(a, b, StringComparison.Ordinal); // false
bool eq2 = string.Equals(a, b, StringComparison.OrdinalIgnoreCase); // false
bool eq3 = string.Equals(a, b, StringComparison.CurrentCultureIgnoreCase); // true
Nos dois primeiros casos, a comparação é byte a byte — sem olhar cultura ou idioma, então ß e SS são diferentes. No terceiro, usa a cultura atual (tipo alemã), e a string é lida como um nativo faria: ß vira ss, e ignora maiúscula/minúscula. Por isso eq3 retorna true.
Como escolher o jeito certo de comparar?
- Ordinal — rápido, byte a byte, bom pra coisas técnicas (tipo comparar IDs).
- CurrentCulture / InvariantCulture — pra textos de usuário, segue as regras do idioma do SO ou da cultura definida.
No Sort, Compare e outros métodos, sempre passa o tipo de comparação:
string.Compare(x, y, StringComparison.CurrentCultureIgnoreCase)
Particularidades de ordenação em idiomas diferentes
Ordenando em russo — ё pode vir depois de е, ou ser igual (depende da cultura!). Então, se for algo sério (tipo um cadastro), pergunta pro cliente ou analista de negócio como tratar essas letras "diferentonas".
3. Proteção contra null: nem todo objeto é bonzinho
O que fazer se o campo for null?
Na vida real, sempre tem alguém que esquece de preencher um campo, aí na comparação — boom! — rola NullReferenceException. Nosso papel é estar preparado.
Exemplo sem proteção contra null:
public int Compare(User x, User y)
{
return x.LastName.CompareTo(y.LastName); // se LastName == null, vai dar ruim!
}
Exemplo com proteção (null menor que qualquer não-null):
public int Compare(User x, User y)
{
// Usa comparador especial de string que lida com null
int byLastName = Comparer<string>.Default.Compare(x.LastName, y.LastName);
if (byLastName != 0) return byLastName;
// e por aí vai...
}
Ainda mais curto:
public int Compare(User x, User y)
{
return string.Compare(x?.LastName, y?.LastName, StringComparison.OrdinalIgnoreCase);
}
"Null" no começo ou no fim?
Dá pra fazer os usuários com sobrenome "vazio" irem pro começo ou pro fim da lista — depende do que você quer.
public int Compare(User x, User y)
{
if (x.LastName == null && y.LastName == null) return 0;
if (x.LastName == null) return 1; // null vai pro fim
if (y.LastName == null) return -1; // null vai pro fim
return string.Compare(x.LastName, y.LastName, StringComparison.OrdinalIgnoreCase);
}
4. Comparando por vários critérios
Erro comum de iniciante — usar conta matemática em vez de "corrente de responsabilidade" na comparação, tipo:
// Não faça isso!
public int Compare(User x, User y)
{
// Exemplo ruim
return (x.Age - y.Age) + string.Compare(x.FirstName, y.FirstName, StringComparison.Ordinal);
}
Esse jeito não garante ordenação certa: se a diferença de idade for -100 e a comparação de string der 1, o resultado vai ser -99, que não faz sentido pra ordenação.
O certo é usar sequência clara:
se já tem diferença — retorna ela, senão olha o próximo critério.
5. Dicas úteis
O que fazer se os objetos forem iguais nos critérios principais?
Se os objetos forem "iguais", é importante que o algoritmo de ordenação seja estável: não muda a ordem dos elementos que são iguais na comparação. O List<T>.Sort() padrão não garante isso. Se for importante (tipo ordenando tabela com vários níveis de ordenação do usuário), usa os métodos LINQ OrderBy/ThenBy — esses são estáveis.
Comparando com campos opcionais/nullable
No .NET, às vezes tem modelo com campo tipo DateTime? (Nullable<DateTime>) ou int?. A lógica é simples: null menor que não-null, ou o contrário — depende do que você quer. Pode usar helpers da lib padrão:
int result = Nullable.Compare<DateTime>(u1.BirthDate, u2.BirthDate);
Comparando com regras extras
Às vezes precisa que a comparação leve em conta o "peso" do critério, tipo clientes VIP sempre primeiro. Solução — põe um "flag VIP" no começo da ordenação.
public int Compare(User x, User y)
{
// VIP primeiro
int vipResult = y.IsVip.CompareTo(x.IsVip); // true = 1, false = 0; ordena decrescente
if (vipResult != 0) return vipResult;
// O resto — normal
int result = string.Compare(x.LastName, y.LastName, StringComparison.OrdinalIgnoreCase);
if (result != 0) return result;
return string.Compare(x.FirstName, y.FirstName, StringComparison.OrdinalIgnoreCase);
}
6. Dicas e melhores práticas
Sempre cheque null
O C# moderno tá cada vez mais "null-safe", mas código antigo não é. Sempre põe proteção ou usa métodos que lidam com isso.
Evite "números mágicos"
Não faz return x.Field - y.Field; pra campos que podem estourar o tipo (overflow). Se for long — pode dar ruim.
Use StringComparison
Não confia no comportamento padrão de comparação de string. Sempre passa StringComparison.OrdinalIgnoreCase ou outro que faça sentido.
Separe comparação e igualdade
As interfaces IComparable<T> e IEqualityComparer<T> servem pra coisas diferentes. Pra ordenar usa comparador, pra buscar únicos — igualdade. Às vezes dão resultado diferente!
Testa casos "estranhos"
Vê se a ordenação funciona quando os campos são iguais, campos vazios, strings com maiúscula/minúscula ou idioma diferente.
GO TO FULL VERSION