No .NET, o GetHashCode
método é usado em muitos lugares nas bibliotecas de classes base do .NET. Implementá-lo adequadamente é especialmente importante para encontrar itens rapidamente em uma coleção ou ao determinar a igualdade.
Existe um algoritmo padrão ou uma prática recomendada sobre como implementar GetHashCode
minhas classes personalizadas para não degradar o desempenho?
.net
algorithm
hashcode
gethashcode
bitbonk
fonte
fonte
GetHashCode
. Espero que seja útil para os outros. Diretrizes e regras para o GetHashCode escritas por Eric LippertGetHashCode()
é usado em muitas implementações deEquals()
. Foi isso que eu quis dizer com essa afirmação.GetHashCode()
insideEquals()
é frequentemente usado como um atalho para determinar a desigualdade , porque se dois objetos têm um código de hash diferente , eles devem ser objetos que não são iguais e o restante da verificação de igualdade não precisa ser executado.GetHashCode()
eEquals()
precisa de olhar para todos os campos de ambos os objetos (Igual tem que fazer isso se os hashcodes são iguais ou não-marcado). Por esse motivo, uma chamada para oGetHashCode()
interiorEquals()
geralmente é redundante e pode reduzir o desempenho.Equals()
também pode causar um curto-circuito, tornando-o muito mais rápido - no entanto, em alguns casos, os códigos de hash podem ser armazenados em cache, tornando aGetHashCode()
verificação mais rápida e valiosa. Veja esta pergunta para mais.Respostas:
Eu costumo usar algo como a implementação dada no fabuloso Java efetivo de Josh Bloch . É rápido e cria um hash muito bom, que provavelmente não causará colisões. Escolha dois números primos diferentes, por exemplo, 17 e 23, e faça:
Conforme observado nos comentários, você pode achar que é melhor escolher um primo grande para multiplicar. Aparentemente, 486187739 é bom ... e, embora a maioria dos exemplos que eu tenha visto com números pequenos tenda a usar números primos, existem pelo menos algoritmos semelhantes nos quais números não primos são frequentemente usados. No exemplo não- FNV mais tarde, por exemplo, usei números que aparentemente funcionam bem - mas o valor inicial não é primo. ( Porém, a constante de multiplicação é primordial. Não sei o quão importante isso é.)
Isso é melhor do que a prática comum de
XOR
inserir códigos de hash por dois motivos principais. Suponha que tenhamos um tipo com doisint
campos:A propósito, o algoritmo anterior é o atualmente usado pelo compilador C # para tipos anônimos.
Esta página oferece algumas opções. Eu acho que, na maioria dos casos, o acima é "bom o suficiente" e é incrivelmente fácil de lembrar e de acertar. A alternativa FNV é igualmente simples, mas usa constantes diferentes e
XOR
nãoADD
como uma operação combinada. Parece algo com o código abaixo, mas o algoritmo FNV normal opera em bytes individuais, portanto, seria necessário modificar para executar uma iteração por byte, em vez do valor de hash de 32 bits. O FNV também foi projetado para comprimentos variáveis de dados, enquanto a maneira como os usamos aqui é sempre para o mesmo número de valores de campo. Os comentários sobre esta resposta sugerem que o código aqui não funciona realmente (no caso de amostra testado) como na abordagem de adição acima.Observe que uma coisa a ter em atenção é que, idealmente, você deve impedir que seu estado sensível à igualdade (e, portanto, sensível ao código de hash) seja alterado após adicioná-lo a uma coleção que depende do código de hash.
Conforme a documentação :
fonte
Dictionary<TKey,TValue>
assume um bom módulo de distribuição de certos primos. E 23 é um deles. Portanto, se você tiver um dicionário com capacidade 23, apenas a última contribuiçãoGetHashCode
influencia o código hash composto. Então, eu prefiro usar 29 em vez de 23.null
- o que não é o mesmo que ignorar o campo.Tipo anônimo
A Microsoft já fornece um bom gerador HashCode genérico: basta copiar os valores de sua propriedade / campo para um tipo anônimo e hash:
Isso funcionará para qualquer número de propriedades. Não usa boxe. Ele apenas usa o algoritmo já implementado na estrutura para tipos anônimos.
ValueTuple - Atualização para C # 7
Como @cactuaroid menciona nos comentários, uma tupla de valor pode ser usada. Isso economiza algumas teclas e, mais importante, é executado exclusivamente na pilha (sem Garbage):
(Nota: a técnica original usando tipos anônimos parece criar um objeto na pilha, ou seja, lixo, pois os tipos anônimos são implementados como classes, embora isso possa ser otimizado pelo compilador. Seria interessante fazer o benchmark dessas opções, mas o opção de tupla deve ser superior.)
fonte
GetHashCode
implementação anônima é muito eficaz (BTW é a mesma da resposta de Jon Skeet), mas o único problema com esta solução é que você gera uma nova instância a qualquerGetHashCode
chamada. Pode ser um pouco sobrecarregado, em particular no caso de acesso intensivo a grandes coleções de hash ...new { PropA, PropB, PropC, PropD }.GetHashCode()
muitoNew With {Key PropA}.GetHashCode()
caso contrário, GetHashCode não retornará o mesmo código de hash para objetos diferentes com as mesmas propriedades de 'identificação'.Aqui está meu ajudante de código de hash.
Sua vantagem é que ele usa argumentos de tipo genérico e, portanto, não causará boxe:
Também possui um método de extensão para fornecer uma interface fluente, para que você possa usá-lo assim:
ou assim:
fonte
T[]
separadamente como já éIEnumerable<T>
Eu tenho uma classe Hashing na biblioteca Helper que a uso para esse fim.
Então, você pode simplesmente usá-lo como:
Como não avaliei o desempenho, qualquer feedback é bem-vindo.
fonte
unchecked
é evitar exceções no estouro desejadasGetHashCode
. Portanto, não está incorreto se o valor exceder o limiteint
e não machucar.null
ser ignorado completamente pode gerar resultados inesperados. Em vez de ignorá-los, você deve usar algum valor constante em vez deinput[i].GetHashCode()
quandoinput[i]
for nulo.Aqui está minha classe auxiliar usando a implementação de Jon Skeet .
Uso:
Se você deseja evitar escrever um método de extensão para System.Int32:
Ele ainda evita qualquer alocação de heap e é usado exatamente da mesma maneira:
Editar (maio de 2018):
EqualityComparer<T>.Default
getter agora é um JIT intrínseco - a solicitação de recebimento é mencionada por Stephen Toub nesta postagem do blog .fonte
var h = Equals(obj, default(T)) ? 0 : obj.GetHashCode();
obj != null
irá compilar com umabox
instrução que alocará memória seT
for um tipo de valor. Em vez disso, você pode usar oobj.Equals(null)
que será compilado em uma chamada virtual doEquals
método.this.hashCode != h
. Não retornaria o mesmo valor..NET Standard 2.1 e superior
Se você estiver usando o .NET Standard 2.1 ou superior, poderá usar a estrutura System.HashCode . Existem dois métodos para usá-lo:
HashCode.Combine
O
Combine
método pode ser usado para criar um código hash, com até oito objetos.HashCode.Add
O
Add
método ajuda você a lidar com coleções:GetHashCode simplificado
Você pode ler a postagem completa do blog ' GetHashCode Made Easy ' para obter mais detalhes e comentários.
Exemplo de uso
Implementação
O que faz um bom algoritmo?
Rapidez
O algoritmo que calcula um código de hash precisa ser rápido. Um algoritmo simples geralmente será mais rápido.
Determinístico
O algoritmo de hash precisa ser determinístico, ou seja, dada a mesma entrada, ele sempre deve produzir a mesma saída.
Reduzir colisões
O algoritmo que calcula um código de hash precisa manter as colisões de hash no mínimo. Uma colisão de hash é uma situação que ocorre quando duas chamadas para
GetHashCode
dois objetos diferentes produzem códigos de hash idênticos. Observe que as colisões são permitidas (algumas têm os conceitos errôneos de que não são), mas devem ser reduzidas ao mínimo.Uma boa função de hash deve mapear as entradas esperadas o mais uniformemente possível sobre seu intervalo de saída. Deve ter uniformidade.
Prevent's DoS
No .NET Core, toda vez que você reinicia um aplicativo, você obtém diferentes códigos de hash. Este é um recurso de segurança para impedir ataques de negação de serviço (DoS). Para o .NET Framework, você deve habilitar esse recurso adicionando o seguinte arquivo App.config:
Devido a esse recurso, os códigos de hash nunca devem ser usados fora do domínio do aplicativo em que foram criados, nunca devem ser usados como campos-chave em uma coleção e nunca devem ser persistidos.
Leia mais sobre isso aqui .
Criptograficamente seguro?
O algoritmo não precisa ser uma função de hash criptográfico . Isso significa que ele não precisa atender às seguintes condições:
fonte
Na maioria dos casos, em que Equals () compara vários campos, não importa se o GetHash () faz hash em um campo ou em muitos. Você só precisa garantir que o cálculo do hash seja realmente barato ( sem alocações , por favor) e rápido ( sem cálculos pesados e certamente sem conexões com o banco de dados) e forneça uma boa distribuição.
O trabalho pesado deve fazer parte do método Equals (); o hash deve ser uma operação muito barata para ativar a chamada Equals () no menor número possível de itens.
E uma dica final: não confie no GetHashCode () como estável em várias execuções de aplicativos . Muitos tipos de .net não garantem que seus códigos de hash permaneçam os mesmos após uma reinicialização; portanto, você deve usar apenas o valor GetHashCode () nas estruturas de dados da memória.
fonte
GetHashCode
executar alocações de memória, desde que o faça somente na primeira vez em que for usado (com chamadas subseqüentes simplesmente retornando um resultado em cache). O importante não é que se faça de tudo para evitar colisões, mas sim que se deve evitar colisões "sistêmicas". Se um tipo tiver doisint
camposoldX
enewX
que diferem frequentemente em um, um valor de hasholdX^newX
atribuiria 90% desses registros a valores de 1, 2, 4 ou 8. O uso deoldX+newX
[aritmética não verificada] pode gerar mais colisões ...Até recentemente, minha resposta teria sido muito próxima da de Jon Skeet aqui. No entanto, iniciei recentemente um projeto que usava tabelas de hash com duas potências, ou seja, tabelas em que o tamanho da tabela interna é 8, 16, 32 etc. Há uma boa razão para favorecer tamanhos de números primos, mas existem Existem também algumas vantagens para tamanhos de dois em dois.
E é muito ruim. Então, depois de um pouco de experimentação e pesquisa, comecei a re-misturar meus hashes com o seguinte:
E então minha tabela de hash de duas potências não foi mais uma droga.
Isso me perturbou, porque o acima não deveria funcionar. Ou, mais precisamente, não deve funcionar, a menos que o original
GetHashCode()
seja ruim de uma maneira muito particular.Re-misturar um código de hash não pode melhorar um ótimo código de hash, porque o único efeito possível é que introduzimos mais algumas colisões.
A mistura de um código hash não pode melhorar um código hash terrível, porque o único efeito possível é alterar, por exemplo, um grande número de colisões no valor 53 para um grande número de valor 18,3487,291.
Misturar novamente um código de hash pode melhorar apenas um código de hash que se saiu pelo menos razoavelmente bem em evitar colisões absolutas em todo o seu intervalo (2 32 valores possíveis), mas muito mal em evitar colisões quando modulado para uso real em uma tabela de hash. Enquanto o módulo mais simples de uma tabela de potências de dois tornava isso mais aparente, também estava tendo um efeito negativo com as tabelas de números primos mais comuns, que não eram tão óbvias (o trabalho extra na reformulação superaria o benefício , mas o benefício ainda estaria lá).
Edit: Eu também estava usando o endereço aberto, o que também teria aumentado a sensibilidade à colisão, talvez mais do que o fato de ser uma potência de dois.
E bem, era perturbador o quanto as
string.GetHashCode()
implementações no .NET (ou estudo aqui ) poderiam ser aprimoradas dessa maneira (na ordem dos testes executados cerca de 20 a 30 vezes mais rápidas devido a menos colisões) e mais perturbador quanto meus próprios códigos de hash poderia ser melhorado (muito mais que isso).Todas as implementações GetHashCode () que eu codifiquei no passado e, de fato, usei como base de respostas neste site, foram muito piores do que eu havia passado . Na maioria das vezes era "bom o suficiente" para muitos usos, mas eu queria algo melhor.
Então, coloquei esse projeto de lado (de qualquer maneira, era um projeto para animais de estimação) e comecei a analisar como produzir rapidamente um bom código de hash bem distribuído no .NET.
No final, resolvi portar o SpookyHash para o .NET. Na verdade, o código acima é uma versão rápida do uso do SpookyHash para produzir uma saída de 32 bits a partir de uma entrada de 32 bits.
Agora, o SpookyHash não é um bom código rápido para lembrar. Meu porto é ainda menos, porque eu escrevi muito sobre ele para obter uma velocidade melhor *. Mas é para isso que serve a reutilização de código.
Depois, coloquei esse projeto de lado, porque, assim como o projeto original havia produzido a questão de como produzir um código hash melhor, esse projeto também produzia a questão de como produzir um melhor memcpy .NET.
Voltei e produzi muitas sobrecargas para alimentar facilmente quase todos os tipos nativos (exceto
decimal
†) em um código hash.É rápido, pelo qual Bob Jenkins merece a maior parte do crédito, porque seu código original de onde eu carreguei é ainda mais rápido, especialmente em máquinas de 64 bits para as quais o algoritmo é otimizado.
O código completo pode ser visto em https://bitbucket.org/JonHanna/spookilysharp/src, mas considere que o código acima é uma versão simplificada dele.
No entanto, como já está escrito, é possível usá-lo com mais facilidade:
Ele também aceita valores de propagação, portanto, se você precisar lidar com informações não confiáveis e desejar proteger contra ataques Hash DoS, poderá definir uma propagação com base no tempo de atividade ou similar e tornar os resultados imprevisíveis pelos invasores:
* Uma grande surpresa nisso é a introdução manual de um método de rotação que retornava
(x << n) | (x >> -n)
itens aprimorados. Eu teria certeza de que o jitter teria indicado isso para mim, mas a criação de perfil mostrou o contrário.†
decimal
não é nativo da perspectiva .NET, embora seja do C #. O problema é que o próprioGetHashCode()
trata a precisão como significativa, enquanto o próprioEquals()
não. Ambos são escolhas válidas, mas não misturadas assim. Ao implementar sua própria versão, você precisa escolher uma ou outra, mas não sei o que você deseja.‡ Como comparação. Se usado em uma string, o SpookyHash em 64 bits é consideravelmente mais rápido do que
string.GetHashCode()
em 32 bits, um pouco mais rápido questring.GetHashCode()
em 64 bits, que é consideravelmente mais rápido que o SpookyHash em 32 bits, mas ainda rápido o suficiente para ser uma escolha razoável.fonte
long
valores para os resultados intermediários e depois reduzir o resultado final para umint
. Parece uma boa ideia? Minha preocupação é que se use, por exemplo, hash = (hash * 31) + nextField, então pares de valores correspondentes afetarão apenas os 27 bits superiores do hash. Permitir que o cálculo se estenda a umlong
material de embalagem minimizaria esse perigo..Update()
com os vários valores conforme a resposta acima fará o truque.Essa é boa:
E aqui está como usá-lo:
fonte
GetHashCode()
método, portanto você sempre pode usá-lo com oparams
parâmetro array. Ou estou faltando alguma coisa aqui?h += (h << 10); h ^= (h >> 6); h += (h << 3); h ^= (h >> 11); h += (h << 15);
ter um codesmell: eles não dependem de qualquer um dos entrada e olhar terrivelmente redundante para mim.A partir de https://github.com/dotnet/coreclr/pull/14863 , existe uma nova maneira de gerar códigos de hash super simples! Apenas escreva
Isso irá gerar um código de hash de qualidade sem que você precise se preocupar com os detalhes da implementação.
fonte
HashCode
alterações no corefx foram mescladas apenas algumas horas antes do seu comentário :) O tipo está previsto para ser lançado no .NET Core 2.1.Aqui está outra implementação fluente do algoritmo publicado acima por Jon Skeet , mas que não inclui alocações ou operações de boxe:
Uso:
O compilador garantirá que
HashValue
não seja chamado com uma classe devido à restrição de tipo genérico. Mas não há suporte para o compilador,HashObject
pois a adição de um argumento genérico também adiciona uma operação de boxe.fonte
Aqui está a minha abordagem simplista. Estou usando o padrão clássico do construtor para isso. É typesafe (sem boxe / unboxing) e também compatível com o .NET 2.0 (sem métodos de extensão etc.).
É usado assim:
E aqui está a classe construtora acutal:
fonte
AddItems<T>(params T[] items)
método com mais frequência na classe auxiliar (do que chamarAddItem(T)
cada vez).this.result * Prime2 * item.GetHashCode()
quando é usado com frequênciathis.result * Prime2 + item.GetHashCode()
?AddItems<T>(params T[] items)
mais frequentemente, porquetypeof(T1) != typeof(T2)
etc.Os usuários do ReSharper podem gerar GetHashCode, Equals e outros com
ReSharper -> Edit -> Generate Code -> Equality Members
.fonte
Se não tivermos mais de 8 propriedades (espero), aqui está outra alternativa.
ValueTuple
é uma estrutura e parece ter umaGetHashCode
implementação sólida .Isso significa que poderíamos simplesmente fazer isso:
Vamos dar uma olhada implementação atual do .NET Núcleo de
ValueTuple
'sGetHashCode
.Isto é de
ValueTuple
:E isso é de
HashHelper
:Em inglês:
Seria bom saber mais sobre as propriedades desse algoritmo de código hash ROL-5.
Lamentavelmente, adiar
ValueTuple
para nós mesmosGetHashCode
pode não ser tão rápido quanto gostaríamos e esperávamos. Este comentário em uma discussão relacionada ilustra que a chamada diretaHashHelpers.Combine
é mais eficiente. Por outro lado, esse é interno, então teríamos que copiar o código, sacrificando muito do que ganhamos aqui. Além disso, seríamos responsáveis por lembrar primeiroCombine
da semente aleatória. Não sei quais são as consequências se pularmos essa etapa.fonte
h1 >> 27
é 0 para ignorá-lo,h1 << 5
é igual a ,h1 * 32
portanto, é o mesmo queh1 * 33 ^ h2
. De acordo com esta página , é chamado "Bernstein modificado".A maior parte do meu trabalho é feita com conectividade de banco de dados, o que significa que todas as minhas classes têm um identificador exclusivo do banco de dados. Eu sempre uso o ID do banco de dados para gerar o código de hash.
fonte
_id.GetHashCode
pois a intenção é clara.Muito parecido com a solução do nightcoder, exceto que é mais fácil criar primos, se você quiser.
PS: Esse é um daqueles momentos em que você vomita um pouco na boca, sabendo que isso poderia ser refatorado em um método com 9 padrões, mas seria mais lento, então você apenas fecha os olhos e tenta esquecê-lo.
fonte
Corri para um problema com carros alegóricos e decimais usando a implementação selecionada como resposta acima.
Este teste falha (flutua; o hash é o mesmo, embora eu tenha alterado 2 valores para ser negativo):
Mas este teste passa (com ints):
Alterei minha implementação para não usar GetHashCode para os tipos primitivos e parece funcionar melhor
fonte
unchecked
não afetaConvert.ToInt32
:uint
,long
,float
,double
edecimal
podem todos estouro aqui.Microsoft lidera várias formas de hash ...
Eu posso supor que, para vários grandes int, você pode usar isso:
E o mesmo para o tipo múltiplo: todos convertidos primeiro para
int
uso, emGetHashCode()
seguida, os valores int serão xor'ed e o resultado é seu hash.Para aqueles que usam hash como ID (quero dizer, um valor único), o hash é naturalmente limitado a vários dígitos, acho que eram 5 bytes para o algoritmo de hash, pelo menos MD5.
Você pode transformar vários valores em um valor de hash e alguns deles serem iguais, portanto, não o use como um identificador. (talvez algum dia eu vou usar seu componente)
fonte
Esta é uma classe auxiliar estática que implementa a implementação de Josh Bloch; e fornece sobrecargas explícitas para "impedir" o boxe e também para implementar o hash especificamente para as primitivas longas.
Você pode passar uma comparação de cadeias que corresponda à sua implementação igual.
Como a saída Hash é sempre um int, você pode apenas encadear chamadas Hash.
fonte
HashKeysAndValues
método foi corrigido: ele chamaHashKeyAndValue
.Caso você deseje polifill a
HashCode
partir denetstandard2.1
Nota: Se usado com
struct
, ele alocará memória devido ao encaixotamentofonte