GetHashCode e Equals: por que sua sobrescrita é crítica para C#

Treinador Online de Python para Iniciantes

Aprenda Python facilmente sem sobrecarga teórica. Resolva tarefas práticas com verificação automática e escreva código diretamente no navegador.

Start Course

Introdução: o erro que torna seu código lento

\n\n

Qualquer pessoa que já tenha programado em C# e lidado com Dictionary ou HashSet, mais cedo ou mais tarde se deparou com essa pergunta no Stack Overflow. Milhares de visualizações, dezenas de respostas, mas a essência é uma: se você sobrescreveu Equals na sua classe, mas esqueceu o GetHashCode — seu código pode funcionar incorretamente ou ser terrivelmente lento. Por quê? Vamos entender com exemplos reais.

\n\n

Imagine que você está criando um sistema de reserva de passagens aéreas. Você tem uma classe Flight com um número de voo único e uma data. Você quer usar essa classe como chave em um Dictionary. Parece simples. Mas sem um GetHashCode correto, seu dicionário pode se transformar em uma lista degenerada com velocidade de busca O(n) em vez de O(1).

\n\n

A mágica das tabelas hash: como o Dictionary funciona em C#

\n\n

Para entender o problema, precisamos olhar sob o capô do Dictionary. Internamente, é um array de "baldes" (buckets). Quando você adiciona um elemento, o C# chama o método GetHashCode() da chave para determinar o número do balde. Se o balde já tiver elementos, o Equals() é chamado para comparar com cada um deles. É essa dupla que garante a busca relâmpago.

\n\n

O contrato que não pode ser violado

\n

Em C#, existe um contrato implícito, mas rigoroso:

\n
    \n
  • Se Equals() retornar true para dois objetos, seus códigos hash obrigatoriamente devem ser iguais.
  • \n
  • Se os códigos hash forem diferentes, Equals() nunca será chamado (isso é uma otimização).
  • \n
  • O inverso não é obrigatório: objetos diferentes podem ter o mesmo código hash (colisão).
  • \n
\n\n

Quando você sobrescreve apenas Equals, mas não GetHashCode, você herda a implementação de System.Object, que é baseada na referência de memória. Dois objetos "iguais" pela sua lógica terão códigos hash diferentes. E é aí que começa o inferno.

\n\n

Exemplo do problema: voos que se perderam no dicionário

\n\n

Veja este código. Parece que deveria funcionar, mas não funciona:

\n\n
public class Flight\n{\n    public string FlightNumber { get; }\n    public DateTime Date { get; }\n\n    public Flight(string flightNumber, DateTime date)\n    {\n        FlightNumber = flightNumber;\n        Date = date;\n    }\n\n    public override bool Equals(object obj)\n    {\n        if (obj is Flight other)\n            return FlightNumber == other.FlightNumber && Date == other.Date;\n        return false;\n    }\n    \n    // GetHashCode NÃO foi sobrescrito!\n}\n\n// Uso\nvar flights = new Dictionary<Flight, string>();\nvar f1 = new Flight("SU100", new DateTime(2024, 12, 25));\nvar f2 = new Flight("SU100", new DateTime(2024, 12, 25));\n\nflights[f1] = "Moscou - Paris";\n\n// Erro! f2 não foi encontrado, embora f1.Equals(f2) == true\nConsole.WriteLine(flights.ContainsKey(f2)); // False
\n\n

Por que isso acontece? f1 e f2 são objetos diferentes na memória. Eles têm códigos hash diferentes de Object.GetHashCode(). O dicionário coloca f1 no balde nº 123 e procura f2 no balde nº 456. Equals nem é chamado! Resultado: elemento não encontrado.

\n\n

Problema de desempenho: quando tudo funciona, mas é lento

\n\n

O pior é uma solução parcial. Digamos que você sobrescreveu GetHashCode, mas fez isso mal, por exemplo, retornando uma constante:

\n\n
public override int GetHashCode() => 42; // Todos os objetos no mesmo balde
\n\n

Formalmente, o contrato é respeitado (objetos iguais têm o mesmo hash). Mas o desempenho despenca: todos os objetos vão para o mesmo balde, e o Dictionary se degenera em uma lista. Inserção e busca se tornam O(n). Para um dicionário com 10.000 elementos, isso é um desastre.

\n\n

Como sobrescrever GetHashCode corretamente?

\n\n

Blogs

Book Recommendations