Como não consegui encontrar uma resposta que explique por que devemos substituir GetHashCode
e Equals
para estruturas personalizadas e por que a implementação padrão "provavelmente não é adequada para uso como chave em uma tabela de hash", deixarei um link para este blog. post , o que explica por que, com um exemplo real de um problema que aconteceu.
Eu recomendo a leitura do post inteiro, mas aqui está um resumo (ênfase e esclarecimentos adicionados).
Motivo pelo qual o hash padrão para estruturas é lento e não muito bom:
A maneira como o CLR é projetado, toda chamada para um membro definido System.ValueType
ou System.Enum
digita [pode] causar uma alocação de boxe [...]
Um implementador de uma função hash enfrenta um dilema: faça uma boa distribuição da função hash ou agilize-a. Em alguns casos, é possível alcançar os dois, mas é difícil fazer isso genericamente no ValueType.GetHashCode
.
A função de hash canônico de uma estrutura "combina" códigos de hash de todos os campos. Mas a única maneira de obter um código de hash de um campo em um ValueType
método é usar a reflexão . Assim, os autores do CLR decidiram negociar a velocidade pela distribuição e a GetHashCode
versão padrão apenas retorna um código de hash de um primeiro campo não nulo e o "mescla" com uma identificação de tipo. [...] Esse é um comportamento razoável, a menos que não seja . Por exemplo, se você não tiver o suficiente e o primeiro campo da sua estrutura tiver o mesmo valor para a maioria das instâncias, uma função hash fornecerá o mesmo resultado o tempo todo. E, como você pode imaginar, isso causará um impacto drástico no desempenho se essas instâncias forem armazenadas em um conjunto de hash ou tabela de hash.
[...] A implementação baseada em reflexão é lenta . Muito devagar.
[...] Ambos ValueType.Equals
e ValueType.GetHashCode
tem uma otimização especial. Se um tipo não possui "ponteiros" e está devidamente compactado, [...] são utilizadas versões mais ideais: GetHashCode
itera sobre uma instância e blocos XORs de 4 bytes e o Equals
método compara duas instâncias usando memcmp
. [...] Mas a otimização é muito complicada. Primeiro, é difícil saber quando a otimização está ativada. [...] Segundo, uma comparação de memória não fornecerá necessariamente os resultados certos . Aqui está um exemplo simples: [...] -0.0
e +0.0
são iguais, mas têm diferentes representações binárias.
Problema do mundo real descrito no post:
private readonly HashSet<(ErrorLocation, int)> _locationsWithHitCount;
readonly struct ErrorLocation
{
// Empty almost all the time
public string OptionalDescription { get; }
public string Path { get; }
public int Position { get; }
}
Usamos uma tupla que continha uma estrutura personalizada com implementação de igualdade padrão. E , infelizmente, a estrutura teve um primeiro campo opcional quase sempre igual a [string vazia] . O desempenho foi bom até o número de elementos no conjunto aumentar significativamente, causando um problema real de desempenho, levando alguns minutos para inicializar uma coleção com dezenas de milhares de itens.
Portanto, para responder à pergunta "em quais casos eu devo compactar minha conta e em quais casos posso confiar com segurança na implementação padrão", pelo menos no caso de estruturas , você deve substituir Equals
e GetHashCode
sempre que sua estrutura personalizada puder ser usada como um digite uma tabela de hash ou Dictionary
.
Eu também recomendaria implementar IEquatable<T>
neste caso, para evitar o boxe.
Como as outras respostas disseram, se você está escrevendo uma classe , o hash padrão usando a igualdade de referência geralmente é bom, então eu não me incomodaria nesse caso, a menos que você precise substituir Equals
(então você deve substituir em GetHashCode
conformidade).