Qual é o melhor algoritmo para substituir GetHashCode?

1449

No .NET, o GetHashCodemé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 GetHashCodeminhas classes personalizadas para não degradar o desempenho?

bitbonk
fonte
38
Depois de ler esta pergunta e o artigo abaixo, eu poderia implementar a substituição de GetHashCode. Espero que seja útil para os outros. Diretrizes e regras para o GetHashCode escritas por Eric Lippert
rene
4
"ou para determinar a igualdade": não! Dois objetos com o mesmo código de hash não são necessariamente iguais.
Thomas Levesque
1
@ThomasLevesque Você está certo, dois objetos com o mesmo código de hash não são necessariamente iguais. Mas ainda GetHashCode()é usado em muitas implementações de Equals(). Foi isso que eu quis dizer com essa afirmação. GetHashCode()inside Equals()é 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.
Bitbonk 02/09/2015
3
@bitbonk Normalmente, tanto GetHashCode()e Equals()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 o GetHashCode()interior Equals()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 a GetHashCode()verificação mais rápida e valiosa. Veja esta pergunta para mais.
precisa saber é o seguinte
ATUALIZAÇÃO EM JANEIRO DE 2020: o blog de Eric Lippert localizado em: docs.microsoft.com/en-us/archive/blogs/ericlippert/…
Rick Davin

Respostas:

1604

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:

public override int GetHashCode()
{
    unchecked // Overflow is fine, just wrap
    {
        int hash = 17;
        // Suitable nullity checks etc, of course :)
        hash = hash * 23 + field1.GetHashCode();
        hash = hash * 23 + field2.GetHashCode();
        hash = hash * 23 + field3.GetHashCode();
        return hash;
    }
}

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 XORinserir códigos de hash por dois motivos principais. Suponha que tenhamos um tipo com dois intcampos:

XorHash(x, x) == XorHash(y, y) == 0 for all x, y
XorHash(x, y) == XorHash(y, x) for all x, y

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 XORnão ADDcomo 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.

// Note: Not quite FNV!
public override int GetHashCode()
{
    unchecked // Overflow is fine, just wrap
    {
        int hash = (int) 2166136261;
        // Suitable nullity checks etc, of course :)
        hash = (hash * 16777619) ^ field1.GetHashCode();
        hash = (hash * 16777619) ^ field2.GetHashCode();
        hash = (hash * 16777619) ^ field3.GetHashCode();
        return hash;
    }
}

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 :

Você pode substituir GetHashCode por tipos de referência imutáveis. Em geral, para tipos de referência mutáveis, você deve substituir GetHashCode apenas se:

  • Você pode calcular o código hash a partir de campos que não são mutáveis; ou
  • Você pode garantir que o código de hash de um objeto mutável não seja alterado enquanto o objeto estiver contido em uma coleção que depende de seu código de hash.
Jon Skeet
fonte
8
O algoritmo descrito no livro que você mencionou é um pouco mais detalhado, principalmente o que fazer para diferentes tipos de dados dos campos. Por exemplo: para campos do tipo long use (int) (campo ^ f >>> 32) em vez de simplesmente chamar GetHashcode. O long.GetHashCodes é implementado dessa maneira?
bitbonk
13
Sim, Int64.GetHashCode faz exatamente isso. Em Java, isso exigiria boxe, é claro. Isso me lembra - hora de adicionar um link para o livro ...
Jon Skeet
77
23 não é uma boa escolha, já que (a partir do .net 3.5 SP1) 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ção GetHashCodeinfluencia o código hash composto. Então, eu prefiro usar 29 em vez de 23.
CodesInChaos
23
@CodeInChaos: Apenas a última contribuição influencia o bucket - portanto, na pior das hipóteses, é necessário analisar todas as 23 entradas do dicionário. Ainda vai verificar o código hash real de cada entrada, o que será barato. Se você tem um dicionário tão pequeno, é improvável que importe muito.
quer
20
@Vajda: Eu costumo usar 0 como o código hash eficaz null- o que não é o mesmo que ignorar o campo.
precisa
431

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:

new { PropA, PropB, PropC, PropD }.GetHashCode();

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):

(PropA, PropB, PropC, PropD).GetHashCode();

(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.)

Rick Love
fonte
85
Sim, a GetHashCodeimplementaçã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 qualquer GetHashCodechamada. Pode ser um pouco sobrecarregado, em particular no caso de acesso intensivo a grandes coleções de hash ...
digEmAll
5
@digEmAll Bom ponto, não pensei na sobrecarga de criar um novo objeto. A resposta de Jon Skeet é a mais eficiente e não usa boxe. (@Kumba Para resolver o verificado em VB, basta usar um Int64 (comprimento) e truncar-lo após os cálculos.)
Rick Love
42
poderia apenas dizer new { PropA, PropB, PropC, PropD }.GetHashCode()muito
sehe
17
O VB.NET deve usar Key na criação do tipo anônimo: New 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'.
David Osborne
4
@ Keith, nesse caso, eu consideraria salvar o IEnumerable como um valor de lista em algum lugar, em vez de enumerá-lo toda vez que o código hash for calculado. Cálculo de ToList toda vez no GetHashCode pode prejudicar o desempenho em muitas situações.
Rick Love
105

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:

public static class HashHelper
{
    public static int GetHashCode<T1, T2>(T1 arg1, T2 arg2)
    {
         unchecked
         {
             return 31 * arg1.GetHashCode() + arg2.GetHashCode();
         }
    }

    public static int GetHashCode<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3)
    {
        unchecked
        {
            int hash = arg1.GetHashCode();
            hash = 31 * hash + arg2.GetHashCode();
            return 31 * hash + arg3.GetHashCode();
        }
    }

    public static int GetHashCode<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, 
        T4 arg4)
    {
        unchecked
        {
            int hash = arg1.GetHashCode();
            hash = 31 * hash + arg2.GetHashCode();
            hash = 31 * hash + arg3.GetHashCode();
            return 31 * hash + arg4.GetHashCode();
        }
    }

    public static int GetHashCode<T>(T[] list)
    {
        unchecked
        {
            int hash = 0;
            foreach (var item in list)
            {
                hash = 31 * hash + item.GetHashCode();
            }
            return hash;
        }
    }

    public static int GetHashCode<T>(IEnumerable<T> list)
    {
        unchecked
        {
            int hash = 0;
            foreach (var item in list)
            {
                hash = 31 * hash + item.GetHashCode();
            }
            return hash;
        }
    }

    /// <summary>
    /// Gets a hashcode for a collection for that the order of items 
    /// does not matter.
    /// So {1, 2, 3} and {3, 2, 1} will get same hash code.
    /// </summary>
    public static int GetHashCodeForOrderNoMatterCollection<T>(
        IEnumerable<T> list)
    {
        unchecked
        {
            int hash = 0;
            int count = 0;
            foreach (var item in list)
            {
                hash += item.GetHashCode();
                count++;
            }
            return 31 * hash + count.GetHashCode();
        }
    }

    /// <summary>
    /// Alternative way to get a hashcode is to use a fluent 
    /// interface like this:<br />
    /// return 0.CombineHashCode(field1).CombineHashCode(field2).
    ///     CombineHashCode(field3);
    /// </summary>
    public static int CombineHashCode<T>(this int hashCode, T arg)
    {
        unchecked
        {
            return 31 * hashCode + arg.GetHashCode();   
        }
    }

Também possui um método de extensão para fornecer uma interface fluente, para que você possa usá-lo assim:

public override int GetHashCode()
{
    return HashHelper.GetHashCode(Manufacturer, PartN, Quantity);
}

ou assim:

public override int GetHashCode()
{
    return 0.CombineHashCode(Manufacturer)
        .CombineHashCode(PartN)
        .CombineHashCode(Quantity);
}
nightcoder
fonte
5
Não há necessidade de T[]separadamente como já éIEnumerable<T>
Nawfal
5
Você poderia refatorar esses métodos e restringir à lógica do núcleo de uma função
Nawfal
12
Aliás, 31 é uma mudança e subtração na CPU, que é extremamente rápida.
Chui Tey
4
@ nightcoder você pode usar parâmetros .
ANeves
6
@ChuiTey Isso é algo que todos os Mersenne Primes têm em comum.
Pharap
63

Eu tenho uma classe Hashing na biblioteca Helper que a uso para esse fim.

/// <summary> 
/// This is a simple hashing function from Robert Sedgwicks Hashing in C book.
/// Also, some simple optimizations to the algorithm in order to speed up
/// its hashing process have been added. from: www.partow.net
/// </summary>
/// <param name="input">array of objects, parameters combination that you need
/// to get a unique hash code for them</param>
/// <returns>Hash code</returns>
public static int RSHash(params object[] input)
{
    const int b = 378551;
    int a = 63689;
    int hash = 0;

    // If it overflows then just wrap around
    unchecked
    {
        for (int i = 0; i < input.Length; i++)
        {
            if (input[i] != null)
            {
                hash = hash * a + input[i].GetHashCode();
                a = a * b;
            }
        }
    }

    return hash;
}

Então, você pode simplesmente usá-lo como:

public override int GetHashCode()
{
    return Hashing.RSHash(_field1, _field2, _field3);
}

Como não avaliei o desempenho, qualquer feedback é bem-vindo.

Wahid Shalaly
fonte
26
Bem, isso causará boxe, se os campos forem do tipo valor.
nightcoder
5
"pode ​​ser aprimorado mais tarde capturando o OverflowException" O objetivo principal uncheckedé evitar exceções no estouro desejadas GetHashCode. Portanto, não está incorreto se o valor exceder o limite inte não machucar.
Tim Schmelter
1
Um problema com esse algoritmo é que qualquer conjunto completo de valores nulos sempre retornará 0, independentemente do seu comprimento
Nathan Adams
2
Este método auxiliar também aloca um novo objeto []
James Newton-King
1
Como o @NathanAdams menciona, o fato de nullser ignorado completamente pode gerar resultados inesperados. Em vez de ignorá-los, você deve usar algum valor constante em vez de input[i].GetHashCode()quando input[i]for nulo.
David Schwartz
58

Aqui está minha classe auxiliar usando a implementação de Jon Skeet .

public static class HashCode
{
    public const int Start = 17;

    public static int Hash<T>(this int hash, T obj)
    {
        var h = EqualityComparer<T>.Default.GetHashCode(obj);
        return unchecked((hash * 31) + h);
    }
}

Uso:

public override int GetHashCode()
{
    return HashCode.Start
        .Hash(_field1)
        .Hash(_field2)
        .Hash(_field3);
}

Se você deseja evitar escrever um método de extensão para System.Int32:

public readonly struct HashCode
{
    private readonly int _value;

    public HashCode(int value) => _value = value;

    public static HashCode Start { get; } = new HashCode(17);

    public static implicit operator int(HashCode hash) => hash._value;

    public HashCode Hash<T>(T obj)
    {
        var h = EqualityComparer<T>.Default.GetHashCode(obj);
        return unchecked(new HashCode((_value * 31) + h));
    }

    public override int GetHashCode() => _value;
}

Ele ainda evita qualquer alocação de heap e é usado exatamente da mesma maneira:

public override int GetHashCode()
{
    // This time `HashCode.Start` is not an `Int32`, it's a `HashCode` instance.
    // And the result is implicitly converted to `Int32`.
    return HashCode.Start
        .Hash(_field1)
        .Hash(_field2)     
        .Hash(_field3);
}

Editar (maio de 2018): EqualityComparer<T>.Defaultgetter agora é um JIT intrínseco - a solicitação de recebimento é mencionada por Stephen Toub nesta postagem do blog .

Şafak Gür
fonte
1
Eu mudaria a linha com o operador terciário para: #var h = Equals(obj, default(T)) ? 0 : obj.GetHashCode();
Bill Barry
Acredito que o operador ternário com obj != nullirá compilar com uma boxinstrução que alocará memória se Tfor um tipo de valor. Em vez disso, você pode usar o obj.Equals(null)que será compilado em uma chamada virtual do Equalsmétodo.
Martin Liversage
Porque this.hashCode != h. Não retornaria o mesmo valor.
akafak Gür
Desculpe-me, remova meu comentário em vez de editá-lo. É mais benéfico criar uma nova estrutura do que alterar o hashCode para não-somente leitura e fazer: "desmarcado {this.hashCode ^ = h * 397;} retorne isso;" por exemplo?
Erik Karlsson
A imutabilidade tem seus benefícios ( por que as estruturas mutáveis ​​são más? ). Sobre o desempenho, o que eu faço é bem barato, pois não aloca espaço na pilha.
Şafak Gür
30

.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 Combinemétodo pode ser usado para criar um código hash, com até oito objetos.

public override int GetHashCode() => HashCode.Combine(this.object1, this.object2);

HashCode.Add

O Addmétodo ajuda você a lidar com coleções:

public override int GetHashCode()
{
    var hashCode = new HashCode();
    hashCode.Add(this.object1);
    foreach (var item in this.collection)
    {
        hashCode.Add(item);
    }
    return hashCode.ToHashCode();
}

GetHashCode simplificado

Você pode ler a postagem completa do blog ' GetHashCode Made Easy ' para obter mais detalhes e comentários.

Exemplo de uso

public class SuperHero
{
    public int Age { get; set; }
    public string Name { get; set; }
    public List<string> Powers { get; set; }

    public override int GetHashCode() =>
        HashCode.Of(this.Name).And(this.Age).AndEach(this.Powers);
}

Implementação

public struct HashCode : IEquatable<HashCode>
{
    private const int EmptyCollectionPrimeNumber = 19;
    private readonly int value;

    private HashCode(int value) => this.value = value;

    public static implicit operator int(HashCode hashCode) => hashCode.value;

    public static bool operator ==(HashCode left, HashCode right) => left.Equals(right);

    public static bool operator !=(HashCode left, HashCode right) => !(left == right);

    public static HashCode Of<T>(T item) => new HashCode(GetHashCode(item));

    public static HashCode OfEach<T>(IEnumerable<T> items) =>
        items == null ? new HashCode(0) : new HashCode(GetHashCode(items, 0));

    public HashCode And<T>(T item) => 
        new HashCode(CombineHashCodes(this.value, GetHashCode(item)));

    public HashCode AndEach<T>(IEnumerable<T> items)
    {
        if (items == null)
        {
            return new HashCode(this.value);
        }

        return new HashCode(GetHashCode(items, this.value));
    }

    public bool Equals(HashCode other) => this.value.Equals(other.value);

    public override bool Equals(object obj)
    {
        if (obj is HashCode)
        {
            return this.Equals((HashCode)obj);
        }

        return false;
    }

    public override int GetHashCode() => this.value.GetHashCode();

    private static int CombineHashCodes(int h1, int h2)
    {
        unchecked
        {
            // Code copied from System.Tuple a good way to combine hashes.
            return ((h1 << 5) + h1) ^ h2;
        }
    }

    private static int GetHashCode<T>(T item) => item?.GetHashCode() ?? 0;

    private static int GetHashCode<T>(IEnumerable<T> items, int startHashCode)
    {
        var temp = startHashCode;

        var enumerator = items.GetEnumerator();
        if (enumerator.MoveNext())
        {
            temp = CombineHashCodes(temp, GetHashCode(enumerator.Current));

            while (enumerator.MoveNext())
            {
                temp = CombineHashCodes(temp, GetHashCode(enumerator.Current));
            }
        }
        else
        {
            temp = CombineHashCodes(temp, EmptyCollectionPrimeNumber);
        }

        return temp;
    }
}

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 GetHashCodedois 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:

<?xml version ="1.0"?>  
<configuration>  
   <runtime>  
      <UseRandomizedStringHashAlgorithm enabled="1" />  
   </runtime>  
</configuration>

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:

  • É inviável gerar uma mensagem que produz um determinado valor de hash
  • É inviável encontrar duas mensagens diferentes com o mesmo valor de hash
  • Uma pequena alteração em uma mensagem deve alterar o valor do hash tão extensivamente que o novo valor do hash apareça sem correlação com o antigo valor do hash (efeito avalanche).
Muhammad Rehan Saeed
fonte
29

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.

Bert Huijben
fonte
10
"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." Este é um conselho perigoso, pois para objetos que diferem apenas nos campos sem hash, você terá colisões de hash. Se isso acontecer com freqüência, o desempenho das coleções baseadas em hash (HashMap, HashSet etc.) será prejudicado (até O (n) no pior caso).
sleske
10
Na verdade, isso aconteceu em Java: nas versões anteriores do JDK String.hashCode () considerava apenas o início da string; isso leva a problemas de desempenho se você usar Strings como chaves no HashMaps, que diferem apenas no final (o que é comum, por exemplo, para URLs). O algoritmo foi, portanto, alterado (no JDK 1.2 ou 1.3, acredito).
sleske
3
Se esse campo "fornecer uma boa distribuição" (última parte da minha resposta), então um campo será suficiente. Se ele não fornecer uma boa distribuição , será necessário outro cálculo (e só então). (Por exemplo, basta usar outro campo que não fornecem uma boa distribuição, ou usar vários campos)
Bert Huijben
Eu não acho que exista um problema em GetHashCodeexecutar 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 dois intcampos oldXe newXque diferem frequentemente em um, um valor de hash oldX^newXatribuiria 90% desses registros a valores de 1, 2, 4 ou 8. O uso de oldX+newX[aritmética não verificada] pode gerar mais colisões ...
supercat
1
... do que a função mais sofisticada, mas uma coleção de 1.000.000 de coisas que possuem 500.000 valores diferentes de hash será muito bem se cada valor de hash tiver duas coisas associadas e muito mal se um valor de hash tiver 500.001 coisas e os outros tiverem uma cada.
Supercat 07/09
23

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:

public static int ReHash(int source)
{
  unchecked
  {
    ulong c = 0xDEADBEEFDEADBEEF + (ulong)source;
    ulong d = 0xE2ADBEEFDEADBEEF ^ c;
    ulong a = d += c = c << 15 | c >> -15;
    ulong b = a += d = d << 52 | d >> -52;
    c ^= b += a = a << 26 | a >> -26;
    d ^= c += b = b << 51 | b >> -51;
    a ^= d += c = c << 28 | c >> -28;
    b ^= a += d = d << 9 | d >> -9;
    c ^= b += a = a << 47 | a >> -47;
    d ^= c += b << 54 | b >> -54;
    a ^= d += c << 32 | c >> 32;
    a += d << 25 | d >> -25;
    return (int)(a >> 1);
  }
}

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:

public override int GetHashCode()
{
  var hash = new SpookyHash();
  hash.Update(field1);
  hash.Update(field2);
  hash.Update(field3);
  return hash.Final().GetHashCode();
}

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:

private static long hashSeed0 = Environment.TickCount;
private static long hashSeed1 = DateTime.Now.Ticks;
public override int GetHashCode()
{
  //produce different hashes ever time this application is restarted
  //but remain consistent in each run, so attackers have a harder time
  //DoSing the hash tables.
  var hash = new SpookyHash(hashSeed0, hashSeed1);
  hash.Update(field1);
  hash.Update(field2);
  hash.Update(field3);
  return hash.Final().GetHashCode();
}

* 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.

decimalnão é nativo da perspectiva .NET, embora seja do C #. O problema é que o próprio GetHashCode()trata a precisão como significativa, enquanto o próprio Equals()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 que string.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.

Jon Hanna
fonte
Ao combinar vários valores de hash em um, eu costumo usar longvalores para os resultados intermediários e depois reduzir o resultado final para um int. 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 um longmaterial de embalagem minimizaria esse perigo.
supercat
@ supercat depende da distribuição do seu munging final. A biblioteca SpookilySharp garantiria que a distribuição fosse boa, idealmente (porque não precisará de criação de objeto), passando um ponteiro para um tipo blittable ou passando um dos enumeráveis ​​que ele manipula diretamente, mas se você ainda não tiver blittable dados ou uma enumeração adequada, a chamada .Update()com os vários valores conforme a resposta acima fará o truque.
Jon Hanna
@JonHanna, você gostaria de ser mais preciso com o comportamento problemático que encontrou? Eu estou tentando implementar uma biblioteca que torna a implementação de objetos de valor trivial ( ValueUtils ) e eu adoraria um conjunto de testes que demonstrasse pouca miscibilidade de hash em tabelas de hash de duas potências.
Eamon Nerbonne
@EamonNerbonne Eu realmente não tenho nada mais preciso do que "o tempo total foi mais lento dessa maneira". Como adicionei em uma edição, o fato de eu estar usando o endereço aberto pode ter sido mais importante do que o fator de potência de dois. Eu pretendo fazer alguns casos de teste em um projeto específico em que compararei algumas abordagens diferentes, para que eu possa ter uma resposta melhor para você depois disso, embora isso não seja de alta prioridade (um projeto pessoal sem necessidade urgente) , então eu vou chegar a ele quando eu chegar a ele ...)
Jon Hanna
@ JonHanna: sim, eu sei como vai o cronograma pessoal do projeto - boa sorte! De qualquer forma, vejo que não expressei bem esse último comentário: pretendia pedir informações problemáticas, e não necessariamente os detalhes dos problemas resultantes. Eu adoraria usar isso como um conjunto de testes (ou inspiração para um conjunto de testes). De qualquer forma - boa sorte com seu projeto de estimação :-).
Eamon Nerbonne
13

Essa é boa:

/// <summary>
/// Helper class for generating hash codes suitable 
/// for use in hashing algorithms and data structures like a hash table. 
/// </summary>
public static class HashCodeHelper
{
    private static int GetHashCodeInternal(int key1, int key2)
    {
        unchecked
        {
           var num = 0x7e53a269;
           num = (-1521134295 * num) + key1;
           num += (num << 10);
           num ^= (num >> 6);

           num = ((-1521134295 * num) + key2);
           num += (num << 10);
           num ^= (num >> 6);

           return num;
        }
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="arr">An array of objects used for generating the 
    /// hash code.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode(params object[] arr)
    {
        int hash = 0;
        foreach (var item in arr)
            hash = GetHashCodeInternal(hash, item.GetHashCode());
        return hash;
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <param name="obj3">The third object.</param>
    /// <param name="obj4">The fourth object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and
    /// data structures like a hash table.
    /// </returns>
    public static int GetHashCode<T1, T2, T3, T4>(T1 obj1, T2 obj2, T3 obj3,
        T4 obj4)
    {
        return GetHashCode(obj1, GetHashCode(obj2, obj3, obj4));
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <param name="obj3">The third object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode<T1, T2, T3>(T1 obj1, T2 obj2, T3 obj3)
    {
        return GetHashCode(obj1, GetHashCode(obj2, obj3));
    }

    /// <summary>
    /// Returns a hash code for the specified objects
    /// </summary>
    /// <param name="obj1">The first object.</param>
    /// <param name="obj2">The second object.</param>
    /// <returns>
    /// A hash code, suitable for use in hashing algorithms and data 
    /// structures like a hash table. 
    /// </returns>
    public static int GetHashCode<T1, T2>(T1 obj1, T2 obj2)
    {
        return GetHashCodeInternal(obj1.GetHashCode(), obj2.GetHashCode());
    }
}

E aqui está como usá-lo:

private struct Key
{
    private Type _type;
    private string _field;

    public Type Type { get { return _type; } }
    public string Field { get { return _field; } }

    public Key(Type type, string field)
    {
        _type = type;
        _field = field;
    }

    public override int GetHashCode()
    {
        return HashCodeHelper.GetHashCode(_field, _type);
    }

    public override bool Equals(object obj)
    {
        if (!(obj is Key))
            return false;
        var tf = (Key)obj;
        return tf._field.Equals(_field) && tf._type.Equals(_type);
    }
}
Magnus
fonte
1
Como são determinadas as chaves? GetHashCode () não aceita nenhum parâmetro, portanto, é necessário chamar esse com duas chaves que precisam ser determinadas de alguma forma. Desculpe, sem mais explicações, isso só parece inteligente, mas não tão bom.
Michael Stum
E por que você precisa das sobrecargas genéricas? O tipo não é importante (e não é usado no seu código), pois todos os objetos têm um GetHashCode()método, portanto você sempre pode usá-lo com o paramsparâmetro array. Ou estou faltando alguma coisa aqui?
gehho
4
Quando você usava objeto em vez de genéricos, obtinha alocações de boxe e memória, que você não deseja em GetHashCode. Portanto, os genéricos são o caminho a percorrer.
precisa saber é o seguinte
1
A mudança de fuga / XOR passos ( 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.
sehe
1
@ Magnus sim, certo, vou excluir meu comentário original. Apenas uma pequena nota de que isso pode não ser tão rápido quanto algumas outras soluções aqui, mas, como você diz, não importa. A distribuição é ótima, melhor do que a maioria das soluções aqui, então +1 de mim! :)
Nawfal
11

A partir de https://github.com/dotnet/coreclr/pull/14863 , existe uma nova maneira de gerar códigos de hash super simples! Apenas escreva

public override int GetHashCode()
    => HashCode.Combine(field1, field2, field3);

Isso irá gerar um código de hash de qualidade sem que você precise se preocupar com os detalhes da implementação.

James Ko
fonte
Parece uma adição agradável ... alguma maneira de saber em qual versão do .NET Core será lançada?
Dan J
1
@ DanJ Que feliz coincidência, as HashCodealteraçõ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.
James Ko
Isso é incrível - e bastante tempo de resposta. Votado. :)
Dan J
@DanJ Notícias ainda melhores - ele deve estar disponível agora nas versões noturnas do CoreFX hospedadas no feed MyGet dotnet-core.
James Ko
Doce - que não me ajuda no trabalho, uma vez que não estamos completamente que o sangramento de ponta, mas é bom saber. Felicidades!
Dan J
9

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:

public static class Hash
{
    public const int Base = 17;

    public static int HashObject(this int hash, object obj)
    {
        unchecked { return hash * 23 + (obj == null ? 0 : obj.GetHashCode()); }
    }

    public static int HashValue<T>(this int hash, T value)
        where T : struct
    {
        unchecked { return hash * 23 + value.GetHashCode(); }
    }
}

Uso:

public class MyType<T>
{
    public string Name { get; set; }

    public string Description { get; set; }

    public int Value { get; set; }

    public IEnumerable<T> Children { get; set; }

    public override int GetHashCode()
    {
        return Hash.Base
            .HashObject(this.Name)
            .HashObject(this.Description)
            .HashValue(this.Value)
            .HashObject(this.Children);
    }
}

O compilador garantirá que HashValuenão seja chamado com uma classe devido à restrição de tipo genérico. Mas não há suporte para o compilador, HashObjectpois a adição de um argumento genérico também adiciona uma operação de boxe.

Scott Wegner
fonte
8

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:

public override int GetHashCode()
{
    HashBuilder b = new HashBuilder();
    b.AddItems(this.member1, this.member2, this.member3);
    return b.Result;
} 

E aqui está a classe construtora acutal:

internal class HashBuilder
{
    private const int Prime1 = 17;
    private const int Prime2 = 23;
    private int result = Prime1;

    public HashBuilder()
    {
    }

    public HashBuilder(int startHash)
    {
        this.result = startHash;
    }

    public int Result
    {
        get
        {
            return this.result;
        }
    }

    public void AddItem<T>(T item)
    {
        unchecked
        {
            this.result = this.result * Prime2 + item.GetHashCode();
        }
    }

    public void AddItems<T1, T2>(T1 item1, T2 item2)
    {
        this.AddItem(item1);
        this.AddItem(item2);
    }

    public void AddItems<T1, T2, T3>(T1 item1, T2 item2, T3 item3)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
    }

    public void AddItems<T1, T2, T3, T4>(T1 item1, T2 item2, T3 item3, 
        T4 item4)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
        this.AddItem(item4);
    }

    public void AddItems<T1, T2, T3, T4, T5>(T1 item1, T2 item2, T3 item3, 
        T4 item4, T5 item5)
    {
        this.AddItem(item1);
        this.AddItem(item2);
        this.AddItem(item3);
        this.AddItem(item4);
        this.AddItem(item5);
    }        

    public void AddItems<T>(params T[] items)
    {
        foreach (T item in items)
        {
            this.AddItem(item);
        }
    }
}
bitbonk
fonte
você pode evitar a criação de objetos na função gethashcode, como na resposta de Mangus. Basta chamar as funções de hash estático (que se importa com o hash inicial). Além disso, você pode usar o AddItems<T>(params T[] items)método com mais frequência na classe auxiliar (do que chamar AddItem(T)cada vez).
Nawfal #
E que benefício você acha de fazer this.result * Prime2 * item.GetHashCode()quando é usado com frequência this.result * Prime2 + item.GetHashCode()?
Nawfal #
Eu não posso usar AddItems<T>(params T[] items)mais frequentemente, porque typeof(T1) != typeof(T2)etc.
bitbonk
ah sim, eu senti falta disso.
Nawfal
5

Os usuários do ReSharper podem gerar GetHashCode, Equals e outros com ReSharper -> Edit -> Generate Code -> Equality Members.

// ReSharper's GetHashCode looks like this
public override int GetHashCode() {
    unchecked {
        int hashCode = Id;
        hashCode = (hashCode * 397) ^ IntMember;
        hashCode = (hashCode * 397) ^ OtherIntMember;
        hashCode = (hashCode * 397) ^ (RefMember != null ? RefMember.GetHashCode() : 0);
        // ...
        return hashCode;
    }
}
Charles Burns
fonte
4

Se não tivermos mais de 8 propriedades (espero), aqui está outra alternativa.

ValueTupleé uma estrutura e parece ter uma GetHashCodeimplementação sólida .

Isso significa que poderíamos simplesmente fazer isso:

// Yay, no allocations and no custom implementations!
public override int GetHashCode() => (this.PropA, this.PropB).GetHashCode();

Vamos dar uma olhada implementação atual do .NET Núcleo de ValueTuple's GetHashCode.

Isto é de ValueTuple:

    internal static int CombineHashCodes(int h1, int h2)
    {
        return HashHelpers.Combine(HashHelpers.Combine(HashHelpers.RandomSeed, h1), h2);
    }

    internal static int CombineHashCodes(int h1, int h2, int h3)
    {
        return HashHelpers.Combine(CombineHashCodes(h1, h2), h3);
    }

E isso é de HashHelper:

    public static readonly int RandomSeed = Guid.NewGuid().GetHashCode();

    public static int Combine(int h1, int h2)
    {
        unchecked
        {
            // RyuJIT optimizes this to use the ROL instruction
            // Related GitHub pull request: dotnet/coreclr#1830
            uint rol5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
            return ((int)rol5 + h1) ^ h2;
        }
    }

Em inglês:

  • Girar à esquerda (deslocamento circular) h1 em 5 posições.
  • Adicione o resultado e h1 juntos.
  • XOR o resultado com h2.
  • Comece executando a operação acima em {static random seed, h1}.
  • Para cada item adicional, execute a operação no resultado anterior e no próximo item (por exemplo, h2).

Seria bom saber mais sobre as propriedades desse algoritmo de código hash ROL-5.

Lamentavelmente, adiar ValueTuplepara nós mesmos GetHashCodepode 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 direta HashHelpers.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 primeiro Combineda semente aleatória. Não sei quais são as consequências se pularmos essa etapa.

Timo
fonte
Assumindo que h1 >> 27é 0 para ignorá-lo, h1 << 5é igual a , h1 * 32portanto, é o mesmo que h1 * 33 ^ h2. De acordo com esta página , é chamado "Bernstein modificado".
Cactuaroid
3

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.

// Unique ID from database
private int _id;

...    
{
  return _id.GetHashCode();
}
Mark G
fonte
Isso significa que se você tiver objetos Pessoa e Conta e ambos tiverem um ID = 1, eles terão o mesmo código de hash. E isso não está bem.
22/03
15
Na verdade, o comentário acima está incorreto. Sempre haverá a possibilidade de colisões de código de hash (um código de hash localiza apenas o bucket, não o objeto individual). Portanto, essa implementação - para um código de hash contendo objetos misturados - levaria a muitas colisões, o que é indesejável, mas seria absolutamente bom se você tivesse objetos de um único tipo em suas tabelas de hash. Também não se distribui uniformemente, no entanto nem a implementação base em System.Object, então eu não me preocuparia muito sobre isso ...
piers7
2
O código hash pode ser apenas o ID, já que o ID é um número inteiro. Não há necessidade de chamar GetHashCode em um inteiro (é uma função de identidade)
Darrel Lee
2
@DarrelLee mas tomo seu _id poderia ser um Guid. É uma boa prática de codificação, _id.GetHashCodepois a intenção é clara.
Nawfal
2
@ 1224, dependendo dos padrões de uso, pode ser horrível pelo motivo que você indica, mas também pode ser ótimo; se você tiver uma sequência desses números sem falhas, terá um hash perfeito, melhor do que qualquer algoritmo pode produzir. Se você sabe que é esse o caso, pode até contar com ele e pular a verificação de igualdade.
Jon Hanna
3

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.

/// <summary>
/// Try not to look at the source code. It works. Just rely on it.
/// </summary>
public static class HashHelper
{
    private const int PrimeOne = 17;
    private const int PrimeTwo = 23;

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();
            hash = hash * PrimeTwo + arg8.GetHashCode();
            hash = hash * PrimeTwo + arg9.GetHashCode();
            hash = hash * PrimeTwo + arg10.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7, T8, T9>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();
            hash = hash * PrimeTwo + arg8.GetHashCode();
            hash = hash * PrimeTwo + arg9.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7, T8>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();
            hash = hash * PrimeTwo + arg8.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6, T7>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();
            hash = hash * PrimeTwo + arg7.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5, T6>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();
            hash = hash * PrimeTwo + arg6.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4, T5>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();
            hash = hash * PrimeTwo + arg5.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();
            hash = hash * PrimeTwo + arg4.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();
            hash = hash * PrimeTwo + arg3.GetHashCode();

            return hash;
        }
    }

    public static int GetHashCode<T1, T2>(T1 arg1, T2 arg2)
    {
        unchecked
        {
            int hash = PrimeOne;
            hash = hash * PrimeTwo + arg1.GetHashCode();
            hash = hash * PrimeTwo + arg2.GetHashCode();

            return hash;
        }
    }
}
Dbl
fonte
2
Não manipula nulos.
JJS 27/12
1

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):

        var obj1 = new { A = 100m, B = 100m, C = 100m, D = 100m};
        var obj2 = new { A = 100m, B = 100m, C = -100m, D = -100m};
        var hash1 = ComputeHash(obj1.A, obj1.B, obj1.C, obj1.D);
        var hash2 = ComputeHash(obj2.A, obj2.B, obj2.C, obj2.D);
        Assert.IsFalse(hash1 == hash2, string.Format("Hashcode values should be different   hash1:{0}  hash2:{1}",hash1,hash2));

Mas este teste passa (com ints):

        var obj1 = new { A = 100m, B = 100m, C = 100, D = 100};
        var obj2 = new { A = 100m, B = 100m, C = -100, D = -100};
        var hash1 = ComputeHash(obj1.A, obj1.B, obj1.C, obj1.D);
        var hash2 = ComputeHash(obj2.A, obj2.B, obj2.C, obj2.D);
        Assert.IsFalse(hash1 == hash2, string.Format("Hashcode values should be different   hash1:{0}  hash2:{1}",hash1,hash2));

Alterei minha implementação para não usar GetHashCode para os tipos primitivos e parece funcionar melhor

    private static int InternalComputeHash(params object[] obj)
    {
        unchecked
        {
            var result = (int)SEED_VALUE_PRIME;
            for (uint i = 0; i < obj.Length; i++)
            {
                var currval = result;
                var nextval = DetermineNextValue(obj[i]);
                result = (result * MULTIPLIER_VALUE_PRIME) + nextval;

            }
            return result;
        }
    }



    private static int DetermineNextValue(object value)
    {
        unchecked
        {

                int hashCode;
                if (value is short
                    || value is int
                    || value is byte
                    || value is sbyte
                    || value is uint
                    || value is ushort
                    || value is ulong
                    || value is long
                    || value is float
                    || value is double
                    || value is decimal)
                {
                    return Convert.ToInt32(value);
                }
                else
                {
                    return value != null ? value.GetHashCode() : 0;
                }
        }
    }
HokieMike
fonte
1
No caso de você destinados outra forma uncheckednão afeta Convert.ToInt32: uint, long, float, doublee decimalpodem todos estouro aqui.
Mark Hurd
1

Microsoft lidera várias formas de hash ...

//for classes that contain a single int value
return this.value;

//for classes that contain multiple int value
return x ^ y;

//for classes that contain single number bigger than int    
return ((int)value ^ (int)(value >> 32)); 

//for classes that contain class instance fields which inherit from object
return obj1.GetHashCode();

//for classes that contain multiple class instance fields which inherit from object
return obj1.GetHashCode() ^ obj2.GetHashCode() ^ obj3.GetHashCode(); 

Eu posso supor que, para vários grandes int, você pode usar isso:

int a=((int)value1 ^ (int)(value1 >> 32));
int b=((int)value2 ^ (int)(value2 >> 32));
int c=((int)value3 ^ (int)(value3 >> 32));
return a ^ b ^ c;

E o mesmo para o tipo múltiplo: todos convertidos primeiro para intuso, em GetHashCode() 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)

deadManN
fonte
7
Xoring inteiros para criar um código de hash é um antipadrão bem conhecido que tende a resultar em um número particularmente alto de colisões com valores do mundo real.
Jon Hanna
Todos aqui usam números inteiros e nunca houve nenhum tipo de garantia para o hash ser o mesmo, apenas tentou variar tanto quanto poucas colisões acontecerem.
deadManN
Sim, mas o seu segundo e quinto não tentam evitar colisões.
Jon Hanna
1
Sim, esse antipadrão é bastante comum.
Jon Hanna
2
Há um equilíbrio a alcançar. Use um código de hash realmente bom, como o Spookyhash, para evitar colisões muito, muito melhores, mas ele terá muito mais tempo de cálculo do que qualquer um deles (mas, quando se trata de misturar grandes quantidades de dados, o Spookyhash é extremamente rápido). Uma simples mudança em um dos valores antes do xoring é apenas um custo extra marginal para uma boa redução na colisão. Multiplicação de números primos, aumentando o tempo e a qualidade novamente. O que é melhor entre shift ou mult é, portanto, discutível. Xor Plain embora muitas vezes tem um monte de colisões em dados reais e é melhor evitar
Jon Hanna
1

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.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.CompilerServices;


namespace Sc.Util.System
{
    /// <summary>
    /// Static methods that allow easy implementation of hashCode. Example usage:
    /// <code>
    /// public override int GetHashCode()
    ///     => HashCodeHelper.Seed
    ///         .Hash(primitiveField)
    ///         .Hsh(objectField)
    ///         .Hash(iEnumerableField);
    /// </code>
    /// </summary>
    public static class HashCodeHelper
    {
        /// <summary>
        /// An initial value for a hashCode, to which is added contributions from fields.
        /// Using a non-zero value decreases collisions of hashCode values.
        /// </summary>
        public const int Seed = 23;

        private const int oddPrimeNumber = 37;


        /// <summary>
        /// Rotates the seed against a prime number.
        /// </summary>
        /// <param name="aSeed">The hash's first term.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static int rotateFirstTerm(int aSeed)
        {
            unchecked {
                return HashCodeHelper.oddPrimeNumber * aSeed;
            }
        }


        /// <summary>
        /// Contributes a boolean to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aBoolean">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, bool aBoolean)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + (aBoolean
                                ? 1
                                : 0);
            }
        }

        /// <summary>
        /// Contributes a char to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aChar">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, char aChar)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + aChar;
            }
        }

        /// <summary>
        /// Contributes an int to the developing HashCode seed.
        /// Note that byte and short are handled by this method, through implicit conversion.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aInt">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, int aInt)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + aInt;
            }
        }

        /// <summary>
        /// Contributes a long to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aLong">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, long aLong)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + (int)(aLong ^ (aLong >> 32));
            }
        }

        /// <summary>
        /// Contributes a float to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aFloat">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, float aFloat)
        {
            unchecked {
                return HashCodeHelper.rotateFirstTerm(aSeed)
                        + Convert.ToInt32(aFloat);
            }
        }

        /// <summary>
        /// Contributes a double to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aDouble">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, double aDouble)
            => aSeed.Hash(Convert.ToInt64(aDouble));

        /// <summary>
        /// Contributes a string to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aString">The value to contribute.</param>
        /// <param name="stringComparison">Optional comparison that creates the hash.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(
                this int aSeed,
                string aString,
                StringComparison stringComparison = StringComparison.Ordinal)
        {
            if (aString == null)
                return aSeed.Hash(0);
            switch (stringComparison) {
                case StringComparison.CurrentCulture :
                    return StringComparer.CurrentCulture.GetHashCode(aString);
                case StringComparison.CurrentCultureIgnoreCase :
                    return StringComparer.CurrentCultureIgnoreCase.GetHashCode(aString);
                case StringComparison.InvariantCulture :
                    return StringComparer.InvariantCulture.GetHashCode(aString);
                case StringComparison.InvariantCultureIgnoreCase :
                    return StringComparer.InvariantCultureIgnoreCase.GetHashCode(aString);
                case StringComparison.OrdinalIgnoreCase :
                    return StringComparer.OrdinalIgnoreCase.GetHashCode(aString);
                default :
                    return StringComparer.Ordinal.GetHashCode(aString);
            }
        }

        /// <summary>
        /// Contributes a possibly-null array to the developing HashCode seed.
        /// Each element may be a primitive, a reference, or a possibly-null array.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aArray">CAN be null.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, IEnumerable aArray)
        {
            if (aArray == null)
                return aSeed.Hash(0);
            int countPlusOne = 1; // So it differs from null
            foreach (object item in aArray) {
                ++countPlusOne;
                if (item is IEnumerable arrayItem) {
                    if (!object.ReferenceEquals(aArray, arrayItem))
                        aSeed = aSeed.Hash(arrayItem); // recursive call!
                } else
                    aSeed = aSeed.Hash(item);
            }
            return aSeed.Hash(countPlusOne);
        }

        /// <summary>
        /// Contributes a possibly-null array to the developing HashCode seed.
        /// You must provide the hash function for each element.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aArray">CAN be null.</param>
        /// <param name="hashElement">Required: yields the hash for each element
        /// in <paramref name="aArray"/>.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash<T>(this int aSeed, IEnumerable<T> aArray, Func<T, int> hashElement)
        {
            if (aArray == null)
                return aSeed.Hash(0);
            int countPlusOne = 1; // So it differs from null
            foreach (T item in aArray) {
                ++countPlusOne;
                aSeed = aSeed.Hash(hashElement(item));
            }
            return aSeed.Hash(countPlusOne);
        }

        /// <summary>
        /// Contributes a possibly-null object to the developing HashCode seed.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="aObject">CAN be null.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int Hash(this int aSeed, object aObject)
        {
            switch (aObject) {
                case null :
                    return aSeed.Hash(0);
                case bool b :
                    return aSeed.Hash(b);
                case char c :
                    return aSeed.Hash(c);
                case int i :
                    return aSeed.Hash(i);
                case long l :
                    return aSeed.Hash(l);
                case float f :
                    return aSeed.Hash(f);
                case double d :
                    return aSeed.Hash(d);
                case string s :
                    return aSeed.Hash(s);
                case IEnumerable iEnumerable :
                    return aSeed.Hash(iEnumerable);
            }
            return aSeed.Hash(aObject.GetHashCode());
        }


        /// <summary>
        /// This utility method uses reflection to iterate all specified properties that are readable
        /// on the given object, excluding any property names given in the params arguments, and
        /// generates a hashcode.
        /// </summary>
        /// <param name="aSeed">The developing hash code, or the seed: if you have no seed, use
        /// the <see cref="Seed"/>.</param>
        /// <param name="aObject">CAN be null.</param>
        /// <param name="propertySelector"><see cref="BindingFlags"/> to select the properties to hash.</param>
        /// <param name="ignorePropertyNames">Optional.</param>
        /// <returns>A hash from the properties contributed to <c>aSeed</c>.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int HashAllProperties(
                this int aSeed,
                object aObject,
                BindingFlags propertySelector
                        = BindingFlags.Instance
                        | BindingFlags.Public
                        | BindingFlags.GetProperty,
                params string[] ignorePropertyNames)
        {
            if (aObject == null)
                return aSeed.Hash(0);
            if ((ignorePropertyNames != null)
                    && (ignorePropertyNames.Length != 0)) {
                foreach (PropertyInfo propertyInfo in aObject.GetType()
                        .GetProperties(propertySelector)) {
                    if (!propertyInfo.CanRead
                            || (Array.IndexOf(ignorePropertyNames, propertyInfo.Name) >= 0))
                        continue;
                    aSeed = aSeed.Hash(propertyInfo.GetValue(aObject));
                }
            } else {
                foreach (PropertyInfo propertyInfo in aObject.GetType()
                        .GetProperties(propertySelector)) {
                    if (propertyInfo.CanRead)
                        aSeed = aSeed.Hash(propertyInfo.GetValue(aObject));
                }
            }
            return aSeed;
        }


        /// <summary>
        /// NOTICE: this method is provided to contribute a <see cref="KeyValuePair{TKey,TValue}"/> to
        /// the developing HashCode seed; by hashing the key and the value independently. HOWEVER,
        /// this method has a different name since it will not be automatically invoked by
        /// <see cref="Hash(int,object)"/>, <see cref="Hash(int,IEnumerable)"/>,
        /// or <see cref="HashAllProperties"/> --- you MUST NOT mix this method with those unless
        /// you are sure that no KeyValuePair instances will be passed to those methods; or otherwise
        /// the generated hash code will not be consistent. This method itself ALSO will not invoke
        /// this method on the Key or Value here if that itself is a KeyValuePair.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="keyValuePair">The value to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int HashKeyAndValue<TKey, TValue>(this int aSeed, KeyValuePair<TKey, TValue> keyValuePair)
            => aSeed.Hash(keyValuePair.Key)
                    .Hash(keyValuePair.Value);

        /// <summary>
        /// NOTICE: this method is provided to contribute a collection of <see cref="KeyValuePair{TKey,TValue}"/>
        /// to the developing HashCode seed; by hashing the key and the value independently. HOWEVER,
        /// this method has a different name since it will not be automatically invoked by
        /// <see cref="Hash(int,object)"/>, <see cref="Hash(int,IEnumerable)"/>,
        /// or <see cref="HashAllProperties"/> --- you MUST NOT mix this method with those unless
        /// you are sure that no KeyValuePair instances will be passed to those methods; or otherwise
        /// the generated hash code will not be consistent. This method itself ALSO will not invoke
        /// this method on a Key or Value here if that itself is a KeyValuePair or an Enumerable of
        /// KeyValuePair.
        /// </summary>
        /// <param name="aSeed">The developing HashCode value or seed.</param>
        /// <param name="keyValuePairs">The values to contribute.</param>
        /// <returns>The new hash code.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static int HashKeysAndValues<TKey, TValue>(
                this int aSeed,
                IEnumerable<KeyValuePair<TKey, TValue>> keyValuePairs)
        {
            if (keyValuePairs == null)
                return aSeed.Hash(null);
            foreach (KeyValuePair<TKey, TValue> keyValuePair in keyValuePairs) {
                aSeed = aSeed.HashKeyAndValue(keyValuePair);
            }
            return aSeed;
        }
    }
}
Steven Coco
fonte
Yipes: Encontrei um bug! O HashKeysAndValuesmétodo foi corrigido: ele chama HashKeyAndValue.
Steven Coco
0

Caso você deseje polifill a HashCodepartir denetstandard2.1

public static class HashCode
{
    public static int Combine(params object[] instances)
    {
        int hash = 17;

        foreach (var i in instances)
        {
            hash = unchecked((hash * 31) + (i?.GetHashCode() ?? 0));
        }

        return hash;
    }
}

Nota: Se usado com struct, ele alocará memória devido ao encaixotamento

Ivan Sanz-Carasa
fonte