Runtime: Adicionar um tipo HashCode para ajudar na combinação de códigos hash

Criado em 25 abr. 2016  ·  206Comentários  ·  Fonte: dotnet/runtime

Substituindo a longa discussão por mais de 200 comentários com o novo problema dotnet / corefx # 14354

Este problema está FECHADO !!!


Motivação

Java tem Objects.hash para combinar rapidamente os códigos hash dos campos constituintes para retornar em Object.hashCode() . Infelizmente, o .NET não tem esse equivalente e os desenvolvedores são forçados a lançar seus próprios hashes como este :

public override int GetHashCode()
{
    unchecked
    {
        int result = 17;
        result = result * 23 + field1.GetHashCode();
        result = result * 23 + field2.GetHashCode();
        return result;
    }
}

Às vezes, as pessoas até recorrem ao uso de Tuple.Create(field1, field2, ...).GetHashCode() para isso, o que é ruim (obviamente), uma vez que aloca.

Proposta

  • Lista de alterações na proposta atual (em relação à última versão aprovada https://github.com/dotnet/corefx/issues/8034#issuecomment-262331783):

    • Empty propriedade adicionada (como ponto de partida natural análogo a ImmutableArray )

    • Nomes de argumentos atualizados: hash -> hashCode , obj -> item

namespace System
{
    public struct HashCode : IEquatable<HashCode>
    {
        public HashCode();

        public static HashCode Empty { get; }

        public static HashCode Create(int hashCode);
        public static HashCode Create<T>(T item);
        public static HashCode Create<T>(T item, IEqualityComparer<T> comparer);

        public HashCode Combine(int hashCode);
        public HashCode Combine<T>(T item);
        public HashCode Combine<T>(T item, IEqualityComparer<T> comparer);

        public int Value { get; }

        public static implicit operator int(HashCode hashCode);

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

        public bool Equals(HashCode other);
        public override bool Equals(object obj);
        public override int GetHashCode();
        public override string ToString();
    }
}

Uso:

`` `c #
int hashCode1 = HashCode.Create (f1) .Combine (f2) .Value;
int hashCode2 = hashes.Aggregate (HashCode.Empty, (seed, hash) => seed.Combine (hash));

var hashCode3 = HashCode.Empty;
foreach (hash int em hashes) {hashCode3 = hashCode3.Combine (hash); }
(int) hashCode3;
`` `

Notas

A implementação deve usar algoritmo em HashHelpers .

Design Discussion api-needs-work area-System.Numerics

Comentários muito úteis

[@redknightlois] Se o que precisamos é uma justificativa de por que ir para System , posso tentar uma justificativa. Construímos HashCode para ajudar nas implementações de object.GetHashCode() , parece adequado que ambos compartilhem o namespace.

Esse foi o raciocínio que @KrzysztofCwalina e eu também usamos. Vendido!

Todos 206 comentários

Se você quiser algo rápido e sujo, pode usar ValueTuple.Create(field1, field2).GetHashCode() . É o mesmo algoritmo usado em Tuple (que, por falar nisso, é semelhante ao de Objects ) e não tem sobrecarga de alocação.

Caso contrário, haverá dúvidas sobre o quão bom um hash você precisará, quais valores de campo prováveis ​​existirão (o que afeta quais algoritmos darão bons ou maus resultados), há uma probabilidade de ataques de hashDoS, colisões módulo a binário- o número par prejudica (como acontece com as tabelas de hash pares binários) e assim por diante, tornando um caso para todos inaplicável.

@JonHanna Acho que essas perguntas também se aplicam a, por exemplo, string.GetHashCode() . Não vejo por que fornecer Hash deve ser mais difícil do que isso.

Na verdade, deveria ser mais simples, já que usuários com requisitos especiais podem facilmente parar de usar Hash , mas parar de usar string.GetHashCode() é mais difícil.

Se você quiser algo rápido e sujo, pode usar ValueTuple.Create (field1, field2) .GetHashCode ().

Ah, boa ideia, eu não tinha pensado em ValueTuple ao fazer este post. Infelizmente, não acho que isso estará disponível até C # 7 / o próximo lançamento do framework, ou mesmo sei se terá esse desempenho (essas chamadas de propriedade / método para EqualityComparer podem somar). Mas eu não fiz nenhum benchmark para medir isso, então eu realmente não sei. Só acho que deveria haver uma classe dedicada / simples para hash que as pessoas pudessem usar sem usar tuplas como uma solução alternativa hackeada.

Caso contrário, haverá dúvidas sobre o quão bom um hash você precisará, quais valores de campo prováveis ​​existirão (o que afeta quais algoritmos darão bons ou maus resultados), há uma probabilidade de ataques de hashDoS, colisões módulo a binário- o número par prejudica (como acontece com as tabelas de hash pares binários) e assim por diante, tornando um caso para todos inaplicável.

Concordo absolutamente, mas não acho que a maioria das implementações leva isso em consideração, por exemplo, ArraySegment a implementação atual é bastante ingênua. O objetivo principal desta classe (junto com evitar alocações) seria fornecer uma implementação inicial para pessoas que não sabem muito sobre hash, para impedi-las de fazer algo estúpido como isso . Pessoas que precisam lidar com as situações que você descreveu podem implementar seu próprio algoritmo de hash.

Infelizmente, não acho que estará disponível até C # 7 / o próximo lançamento do framework

Acho que você pode usá-lo com C # 2, mas não com suporte integrado.

ou mesmo saber se será esse desempenho (essas chamadas de propriedade / método para EqualityComparer podem somar)

O que essa classe faria de diferente? Se chamar obj == null ? 0 : obj.GetHashCode() explicitamente for mais rápido, deve ser movido para ValueTuple .

Eu estaria inclinado a marcar esta proposta com +1 algumas semanas atrás, mas estou menos inclinado à luz de ValueTuple reduzindo a sobrecarga de alocação do truque de usar Tuple para isso, isso parece cair entre dois bancos para mim: se você não precisa de algo especialmente especializado, você pode usar ValueTuple , mas se você precisa de algo além disso, uma aula como esta não irá longe o suficiente.

E quando tivermos C # 7, ele terá o açúcar sintático para torná-lo ainda mais fácil.

@JonHanna

O que essa classe faria de diferente? Se chamar explicitamente obj == null? 0: obj.GetHashCode () é mais rápido do que deveria ser movido para ValueTuple.

Por que não fazer ValueTuple apenas usar a classe Hash para obter códigos hash? Isso também reduziria o LOC no arquivo significativamente (que agora é cerca de 2.000 linhas).

editar:

Se você não precisa de algo especialmente especializado, você pode usar ValueTuple

Verdade, mas o problema é que muitas pessoas podem não perceber isso e implementar sua própria função de hash ingênua inferior (como a que indiquei acima).

Que eu realmente poderia ficar para trás.

Provavelmente fora do escopo deste problema. Mas ter um namespace de hashing onde podemos encontrar hashes criptográficos e não criptográficos de alto desempenho escritos por especialistas seria uma vitória aqui.

Por exemplo, tivemos que codificar xxHash32, xxHash64, Metro128 e também reduzir a resolução de 128 para 64 e de 64 para 32 bits. Ter um conjunto de funções otimizadas pode ajudar os desenvolvedores a evitar escrever suas próprias funções não otimizadas e / ou com bugs (eu sei, encontramos alguns bugs em nós também); mas ainda podendo escolher dependendo das necessidades.

Ficaríamos felizes em doar nossas implementações se houver interesse, para que possam ser revisadas e otimizadas ainda mais por especialistas.

@redknightlois Eu ficaria feliz em adicionar minha implementação SpookyHash a um esforço como esse.

@svick Cuidado com string.GetHashCode (), entretanto, isso é muito específico, por uma boa razão, ataques Hash DoS.

@terrajobst , quão longe isso está em

cc: @ellismg

Acho que está pronto para revisão em seu estado atual.

@mellinoe Isso é ótimo! Limpei um pouco a proposta para torná-la mais concisa e também adicionei algumas perguntas no final que acho que devem ser respondidas.

@jamesqo Deve haver long baseado também.

@redknightlois , parece razoável. Eu atualizei a proposta para incluir sobrecargas de Combine long de Combine .

A sugestão de @JonHanna não é boa o suficiente?

C# return ValueTuple.Create(a, b, c).GetHashCode();

A menos que haja motivos bons o suficiente para que isso não seja bom o suficiente, não achamos que ele está passando pelo corte.

Além do código gerado ser algumas ordens de magnitude pior, não consigo pensar em nenhum outro motivo bom o suficiente. A menos, é claro, que haja otimizações no novo tempo de execução que lidem com esse caso específico em mente, caso em que esta análise é discutível. Tendo dito isso, tentei isso em 1.0.1.

Deixe-me ilustrar com um exemplo.

Vamos supor que pegamos o código real que está sendo usado para ValueTuple e usamos constantes para chamá-lo.

        internal static class HashHelpers
        {
            public static int Combine(int h1, int h2)
            {
                // The jit optimizes this to use the ROL instruction on x86
                // Related GitHub pull request: dotnet/coreclr#1830
                uint shift5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
                return ((int)shift5 + h1) ^ h2;
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryStaticCall()
        {
            return HashHelpers.Combine(10202, 2003);
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryValueTuple()
        {
            return ValueTuple.Create(10202, 2003).GetHashCode();
        }
    }

Agora, em um compilador otimizado, as chances são de que não deveria haver nenhuma diferença, mas na realidade há.

Este é o código real para ValueTuple

image
Então agora o que pode ser visto aqui? Primeiro, estamos criando uma estrutura na pilha e, em seguida, chamando o código hash real.

Agora compare-o com o uso de HashHelper.Combine que, para todos os efeitos, poderia ser a implementação real de Hash.Combine

image

Eu sei!!!
Mas não vamos parar por aí ... vamos usar parâmetros reais:

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryStaticCall(int h1, int h2)
        {
            return HashHelpers.Combine(h1, h2);
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryValueTuple(int h1, int h2)
        {
            return ValueTuple.Create(h1, h2).GetHashCode();
        }

        static unsafe void Main(string[] args)
        {
            var g = new Random();
            int h1 = g.Next();
            int h2 = g.Next(); 
            Console.WriteLine(TryStaticCall(h1, h2));
            Console.WriteLine(TryValueTuple(h1, h2));
        }

image

O bom, isso é extremamente estável. Mas vamos comparar com a alternativa:

image

Agora vamos ao mar ...

        internal static class HashHelpers
        {
            public static int Combine(int h1, int h2)
            {
                // The jit optimizes this to use the ROL instruction on x86
                // Related GitHub pull request: dotnet/coreclr#1830
                uint shift5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
                return ((int)shift5 + h1) ^ h2;
            }
            public static int Combine(int h1, int h2, int h3, int h4)
            {
                return Combine(Combine(h1, h2), Combine(h3, h4));
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryStaticCall(int h1, int h2, int h3, int h4)
        {
            return HashHelpers.Combine(h1, h2, h3, h4);
        }

E o resultado é bem ilustrativo

image

Não posso realmente inspecionar o código real que o JIT gera para a chamada, mas apenas o prólogo e o epílogo são suficientes para justificar a inclusão da proposta.

image

A conclusão da análise é simples: o fato de o tipo de retenção ser struct não significa que seja gratuito :)

O desempenho foi mencionado durante a reunião. A questão é se essa API provavelmente estará no caminho certo. Para ser claro, não estou dizendo que não devemos ter a API. Estou apenas dizendo que, a menos que haja um cenário concreto, é mais difícil projetar a API porque não podemos dizer "precisamos dela para X, portanto, a medida do sucesso é se X pode usá-la". Isso é importante para APIs que não permitem que você faça algo novo, mas sim de uma forma mais otimizada.

Eu acho que quanto mais importante é ter um hash rápido e de boa qualidade, mais importante é ajustar o algoritmo usado para o (s) objeto (s) e a faixa de valores que podem ser vistos e, portanto, quanto mais você precisa disso um ajudante, mais você precisa para não usar tal ajudante.

@terrajobst , o desempenho foi a principal motivação para esta proposta, mas não a única. Ter um tipo dedicado ajudará na descoberta; mesmo com o suporte integrado à tupla no C # 7, os desenvolvedores podem não saber necessariamente que são igualados por valor. Mesmo que o façam, eles podem esquecer que as tuplas substituem GetHashCode e provavelmente terão que pesquisar no Google como implementar GetHashCode no .NET.

Além disso, há um sutil problema de correção com o uso de ValueTuple.Create.GetHashCode . Os últimos 8 elementos, apenas os últimos 8 elementos são hash; o resto é ignorado.

@terrajobst No RavenDB GetHashCode, o desempenho teve um impacto tão grande em nossos resultados financeiros que acabamos implementando um conjunto completo de rotinas altamente otimizadas. Até mesmo Roslyn tem seu próprio hashing interno https://github.com/dotnet/roslyn/blob/master/src/Compilers/Core/Portable/InternalUtilities/Hash.cs também verifique a discussão em Roslyn especificamente aqui: https: // github .com / dotnet / coreclr / issues / 1619 ... Então, quando o desempenho é FUNDAMENTAL, não podemos usar a plataforma fornecida e temos que lançar nossa própria plataforma (e pagar pelas consequências).

Além disso, o problema de

@JonHanna

Eu acho que quanto mais importante é ter um hash rápido e de boa qualidade, mais importante é ajustar o algoritmo usado para o (s) objeto (s) e a faixa de valores que podem ser vistos e, portanto, quanto mais você precisa disso um ajudante, mais você precisa para não usar tal ajudante.

Então você está dizendo que adicionar uma classe auxiliar seria ruim, já que encorajaria as pessoas a apenas adicionar a função auxiliar sem pensar em como fazer um hash adequado?

Parece que o oposto seria verdadeiro, na verdade; Hash.Combine geralmente deve melhorar as implementações de GetHashCode . Pessoas que sabem fazer hashing podem avaliar Hash.Combine para ver se ele se encaixa em seu caso de uso. Iniciantes que realmente não sabem sobre hash usarão Hash.Combine vez de apenas xor-ing (ou pior, adicionar) os campos constituintes porque não sabem como fazer um hash adequado.

Discutimos isso um pouco mais e você nos convenceu :-)

Mais algumas perguntas:

  1. Precisamos decidir onde colocar esse tipo. A introdução de um novo namespace parece estranho; System.Numerics pode funcionar. System.Collections.Generic também pode funcionar, porque tem os comparadores e o hashing é mais frequentemente usado no contexto de coleções.
  2. Devemos fornecer um padrão de construtor livre de alocação para combinar um número desconhecido de códigos hash?

Em (2) @Eilon disse o seguinte:

Para referência, ASP.NET Core (e seus predecessores e projetos relacionados) usam um HashCodeCombiner: https://github.com/aspnet/Common/blob/dev/src/Microsoft.Extensions.HashCodeCombiner.Sources/HashCodeCombiner.cs

( @David Fowler mencionou isso no tópico do GitHub há vários meses.)

E este é um exemplo de uso: https://github.com/aspnet/Mvc/blob/760c8f38678118734399c58c2dac981ea6e47046/src/Microsoft.AspNetCore.Mvc.Razor/Internal/ViewLocationCacheKey.cs#L129 -L144

`` `C #
var hashCodeCombiner = HashCodeCombiner.Start ();
hashCodeCombiner.Add (IsMainPage? 1: 0);
hashCodeCombiner.Add (ViewName, StringComparer.Ordinal);
hashCodeCombiner.Add (ControllerName, StringComparer.Ordinal);
hashCodeCombiner.Add (AreaName, StringComparer.Ordinal);

if (ViewLocationExpanderValues! = null)
{
foreach (item var em ViewLocationExpanderValues)
{
hashCodeCombiner.Add (item.Key, StringComparer.Ordinal);
hashCodeCombiner.Add (item.Value, StringComparer.Ordinal);
}
}

return hashCodeCombiner;
`` `

Discutimos isso um pouco mais e você nos convenceu :-)

🎉

A introdução de um novo namespace parece estranho; System.Numerics pode funcionar.

Se decidirmos não adicionar um novo namespace, deve-se observar que qualquer código que tenha uma classe chamada Hash e uma diretiva using System.Numerics não conseguirá compilar com um erro de tipo ambíguo.

Devemos fornecer um padrão de construtor livre de alocação para combinar um número desconhecido de códigos hash?

Parece uma ótima ideia. Como algumas sugestões iniciais, talvez devêssemos nomeá-lo HashBuilder (a la StringBuilder ) e tê-lo return this após cada método Add para torná-lo mais fácil para adicionar hashes, assim:

public override int GetHashCode()
{
    return HashBuilder.Create(_field1)
        .Add(_field2)
        .Add(_field3)
        .ToHash();
}

@jamesqo atualize a proposta no topo quando houver consenso sobre o tópico. Podemos então fazer a revisão final. Atribuindo a você agora, enquanto você conduz o design ;-)

Se decidirmos não adicionar um novo namespace, deve-se observar que qualquer código que tenha uma classe chamada Hash e uma diretiva using System.Numerics não conseguirá compilar com um erro de tipo ambíguo.

Depende do cenário real. Em muitos casos, o compilador preferirá seu tipo, pois a hierarquia de namespace definida da unidade de compilação é percorrida antes de considerar o uso de diretivas.

Mesmo assim: adicionar APIs pode ser uma mudança importante na fonte. No entanto, é impraticável evitar isso, supondo que queiramos avançar 😄 Geralmente nos esforçamos para evitar conflitos, por exemplo, usando nomes que não são muito genéricos. Por exemplo, não acho que devemos chamar o tipo Hash . Acho que HashCode provavelmente seria melhor.

Como algumas sugestões iniciais, talvez devêssemos chamá-lo de HashBuilder

Como uma primeira aproximação, eu estava pensando em combinar a estática e o construtor em um único tipo, assim:

`` `C #
namespace System.Collections.Generic
{
public struct HashCode
{
public static int Combine (int hash1, int hash2);
public static int Combine (int hash1, int hash2, int hash3);
public static int Combine (int hash1, int hash2, int hash3, int hash4);
public static int Combine (int hash1, int hash2, int hash3, int hash4, int hash5);
public static int Combine (int hash1, int hash2, int hash3, int hash4, int hash5, int hash6);

    public static long Combine(long hash1, long hash2);
    public static long Combine(long hash1, long hash2, long hash3);
    public static long Combine(long hash1, long hash2, long hash3, long hash4);
    public static long Combine(long hash1, long hash2, long hash3, long hash4, long hash5);
    public static long Combine(long hash1, long hash2, long hash3, long hash4, long hash5, longhash6);

    public static int CombineHashCodes<T1, T2>(T1 o1, T2 o2);
    public static int CombineHashCodes<T1, T2, T3>(T1 o1, T2 o2, T3 o3);
    public static int CombineHashCodes<T1, T2, T3, T4>(T1 o1, T2 o2, T3 o3, T4 o4);
    public static int CombineHashCodes<T1, T2, T3, T4, T5>(T1 o1, T2 o2, T3 o3, T4 o4, T5 o5);
    public static int CombineHashCodes<T1, T2, T3, T4, T5, T6>(T1 o1, T2 o2, T3 o3, T4 o4, T5 o5, T6 o6);

    public void Combine(int hashCode);
    public void Combine(long hashCode);
    public void Combine<T>(T obj);
    public void Combine(string text, StringComparison comparison);

    public int Value { get; }
}

}

This allows for code like this:

``` C#
return HashCode.Combine(value1, value2);

assim como:

`` `C #
var hashCode = new HashCode ();
hashCode.Combine (IsMainPage? 1: 0);
hashCode.Combine (ViewName, StringComparer.Ordinal);
hashCode.Combine (ControllerName, StringComparer.Ordinal);
hashCode.Combine (AreaName, StringComparer.Ordinal);

if (ViewLocationExpanderValues! = null)
{
foreach (item var em ViewLocationExpanderValues)
{
hashCode.Combine (item.Key, StringComparer.Ordinal);
hashCode.Combine (item.Value, StringComparer.Ordinal);
}
}

return hashCode.Value;
`` `

Pensamentos?

Eu gosto da ideia de @jamesqo de chamadas em cadeia (retornar this dos métodos de instância Combine ).

Eu iria até mesmo remover completamente os métodos estáticos e manter apenas os métodos de instância ...

Combine(long hashCode) será apenas lançado em int . Nós realmente queremos isso?
Em primeiro lugar, qual é o caso de uso para sobrecargas de long ?

@karelz Por favor, não os remova, as estruturas não são gratuitas. Hashes podem ser usados ​​em caminhos muito quentes, você certamente não quer desperdiçar instruções quando o método estático seria essencialmente gratuito. Veja a análise do código onde mostrei o impacto real da estrutura envolvente.

Usamos a classe estática Hashing para evitar conflitos de nomes e o código parece bom.

@redknightlois Eu me pergunto se devemos esperar o mesmo código 'ruim' também em um caso de struct não genérico com um campo int.
Se esse ainda for um código assembly 'ruim', me pergunto se poderíamos melhorar o JIT para fazer um trabalho melhor nas otimizações aqui. Adicionar APIs apenas para salvar algumas instruções deve ser nosso último recurso, IMO.

@redknightlois Curioso, o JIT gera algum código pior se a estrutura (neste caso HashCode ) caber em um registrador? Será apenas int big.

Além disso, tenho visto muitas solicitações de pull no coreclr recentemente para melhorar o código gerado em torno de structs e parece que dotnet / coreclr # 8057 habilitará essas otimizações. Talvez o código que o JIT gera seja melhor após essa mudança?

editar: vejo que @karelz já mencionou meus pontos aqui.

@karelz , concordo com você - assumindo que o JIT gere um código decente para uma estrutura int (o que eu acredito que sim, ImmutableArray não tem sobrecarga, por exemplo), então as sobrecargas estáticas são redundante e pode ser removido.

@terrajobst Mais algumas ideias que tenho:

  • Acho que podemos combinar um pouco as suas e as minhas ideias. HashCode parece um bom nome; não precisa ser uma estrutura mutável seguindo o padrão do construtor. Em vez disso, pode ser um invólucro imutável ao redor de um int , e cada operação Combine pode retornar um novo HashCode . Por exemplo
public struct HashCode
{
    private readonly int _hash;

    public HashCode Combine(int hash) => return new HashCode(CombineCore(_hash, hash));

    public HashCode Combine<T>(T item) => Combine(EqualityComparer<T>.Default.GetHashCode(item));
}

// Usage
HashCode combined = new HashCode(_field1)
    .Combine(_field2)
    .Combine(_field3);
  • Devemos ter apenas um operador implícito para conversão em int para que as pessoas não precisem ter aquela última .Value chamada.
  • Re Combine , esse é o melhor nome? Parece mais descritivo, mas Add é mais curto e fácil de digitar. ( Mix é outra alternativa, mas é um pouco doloroso de digitar.)

    • public void Combine(string text, StringComparison comparison) : Não acho que realmente pertença ao mesmo tipo, já que não está relacionado a strings. Além disso, é fácil escrever StringComparer.XXX.GetHashCode(str) para aqueles raros momentos em que você precisa fazer isso.

    • Devemos remover as sobrecargas longas deste tipo e ter um tipo HashCode separado para longas. Algo como Int64HashCode ou LongHashCode .

Fiz uma pequena implementação de amostra de coisas no TryRoslyn: http://tinyurl.com/zej9yux

Felizmente, é fácil verificar. E a boa notícia é que funciona bem como está 👍

image

Devemos ter apenas um operador implícito para conversão em int, para que as pessoas não precisem ter a última chamada .Value.

Provavelmente, o código não é tão simples, uma conversão implícita o limparia um pouco. Eu ainda gosto da ideia de poder ter interface de vários parâmetros também.

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryHashCombiner(int h1, int h2, int h3, int h4)
        {
            var h = new HashCode(h1).Combine(h2).Combine(h3).Combine(h4);
            return h.Value;
        }

Re Combine, esse é o melhor nome? Parece mais descritivo, mas Adicionar é mais curto e fácil de digitar. (Mix é outra alternativa, mas é um pouco doloroso de digitar.)

Combine é o nome real usado na comunidade de hashing afaik. E dá uma ideia clara do que está sendo feito.

@jamesqo Existem muitas funções de hashing, tivemos que implementar versões muito rápidas para, de 32bits, 64bits a 128bits para RavenDB (e usamos cada uma para finalidades diferentes).

Podemos pensar no futuro neste design com algum mecanismo extensível como este:

        internal interface IHashCode<T> where T : struct
        {
            T Combine(T h1, T h2);
        }

        internal struct RotateHashCode : IHashCode<int>, IHashCode<long>
        {
            long IHashCode<long>.Combine(long h1, long h2)
            {
                // The jit optimizes this to use the ROL instruction on x86
                // Related GitHub pull request: dotnet/coreclr#1830
                ulong shift5 = ((ulong)h1 << 5) | ((ulong)h1 >> 27);
                return ((int)shift5 + h1) ^ h2;
            }

            int IHashCode<int>.Combine(int h1, int h2)
            {
                // The jit optimizes this to use the ROL instruction on x86
                // Related GitHub pull request: dotnet/coreclr#1830
                uint shift5 = ((uint)h1 << 5) | ((uint)h1 >> 27);
                return ((int)shift5 + h1) ^ h2;
            }
        }

        internal struct HashCodeCombiner<T, W> where T : struct, IHashCode<W>
                                               where W : struct
        {
            private static T hasher;
            public W Value;

            static HashCodeCombiner()
            {
                hasher = new T();
            }

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public HashCodeCombiner(W seed)
            {
                this.Value = seed;
            }

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public HashCodeCombiner<T,W> Combine( W h1 )
            {
                Value = hasher.Combine(this.Value, h1);
                return this;
            }
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        public static int TryHashCombinerT(int h1, int h2, int h3, int h4)
        {
            var h = new HashCodeCombiner<RotateHashCode, int>(h1).Combine(h2).Combine(h3).Combine(h4);
            return h.Value;
        }

Não sei por que o JIT está criando um código de prólogo muito chato para isso. Não deveria, então provavelmente pode ser otimizado, devemos pedir isso aos desenvolvedores do JIT. Mas quanto ao resto, você pode implementar quantos Combiners diferentes desejar, sem perder uma única instrução. Dito isso, esse método é provavelmente mais útil para funções hash reais do que para combinadores. cc @CarolEidt @AndyAyersMS

EDIT: Pensando em voz alta aqui para um mecanismo geral para combinar funções hash cripto e não criptográficas em um único conceito de hash.

@jamesqo

não precisa ser uma estrutura mutável seguindo o padrão do construtor

Ah sim. Nesse caso, estou bem com esse padrão. Em geral, não gosto do padrão de retorno de instâncias se a operação tiver um efeito colateral. É especialmente ruim se a API está seguindo o padrão imutável WithXxx . Nesse caso, porém, o padrão é essencialmente uma estrutura de dados imutável, de modo que esse padrão funcionaria bem.

Acho que podemos combinar um pouco as suas e as minhas ideias.

👍, e quanto a:

`` `C #
public struct HashCode
{
public static HashCode Create(T obj);

[Pure] public HashCode Combine(int hashCode);
[Pure] public HashCode Combine(long hashCode);
[Pure] public HashCode Combine<T>(T obj);
[Pure] public HashCode Combine(string text, StringComparison comparison);

public int Value { get; }

public static implicit operator int(HashCode hashCode);

}

This allows for code like this:

``` C#
public override int GetHashCode()
{
    return HashCode.Create(value1).Combine(value2);
}

assim como isso:

`` `C #
var hashCode = new HashCode ()
.Combine (IsMainPage? 1: 0)
.Combine (ViewName, StringComparer.Ordinal)
.Combine (ControllerName, StringComparer.Ordinal)
.Combine (AreaName, StringComparer.Ordinal);

if (ViewLocationExpanderValues! = null)
{
foreach (item var em ViewLocationExpanderValues)
{
hashCode = hashCode.Combine (item.Key, StringComparer.Ordinal);
hashCode = hashCode.Combine (item.Value, StringComparer.Ordinal);
}
}

return hashCode.Value;
`` `

Pensamentos @terrajobst :

  1. O método de fábrica Create<T> deve ser removido. Caso contrário, haveria 2 maneiras de escrever a mesma coisa, HashCode.Create(_val) ou new HashCode().Combine(_val) . Além disso, ter nomes diferentes para Create / Combine não seria compatível com as diferenças, pois se você adicionasse um novo primeiro campo, teria que alterar 2 linhas.
  2. Não acho que a sobrecarga de aceitar uma string / StringComparison pertença aqui; HashCode não tem nada a ver com strings. Em vez disso, talvez devêssemos adicionar uma GetHashCode(StringComparison) api à string? (Além disso, todas essas são comparações ordinais, que é o comportamento padrão de string.GetHashCode .)
  3. De que adianta ter Value , se já existe um operador implícito para conversão em int ? Novamente, isso faria com que pessoas diferentes escrevessem coisas diferentes.
  4. Temos que mover a sobrecarga de long para um novo tipo. HashCode terá apenas 32 bits de largura; não cabe muito tempo.
  5. Vamos adicionar algumas sobrecargas levando tipos não assinados, uma vez que são mais comuns em hash.

Aqui está minha API proposta:

public struct HashCode
{
    public HashCode Combine(int hash);
    public HashCode Combine(uint hash);
    public HashCode Combine<T>(T obj);

    public static implicit operator int(HashCode hashCode);
    public static implicit operator uint(HashCode hashCode);
}

public struct Int64HashCode
{
    public Int64HashCode Combine(long hash);
    public Int64HashCode Combine(ulong hash);

    public static implicit operator long(Int64HashCode hashCode);
    public static implicit operator ulong(Int64HashCode hashCode);
}

Com apenas esses métodos, o exemplo do ASP.NET ainda pode ser escrito como

var hashCode = new HashCode()
    .Combine(IsMainPage ? 1 : 0)
    .Combine(ViewName)
    .Combine(ControllerName)
    .Combine(AreaName);

if (ViewLocationExpanderValues != null)
{
    foreach (var item in ViewLocationExpanderValues)
    {
        hashCode = hashCode.Combine(item.Key);
        hashCode = hashCode.Combine(item.Value);
    }
}

return hashCode;

@jamesqo

De que adianta ter Value , se já existe um operador implícito para conversão em int ? Novamente, isso faria com que pessoas diferentes escrevessem coisas diferentes.

As diretrizes de design da estrutura para sobrecargas de operador dizem:

CONSIDERE fornecer métodos com nomes amigáveis ​​que correspondam a cada operador sobrecarregado.

Muitos idiomas não oferecem suporte à sobrecarga do operador. Por esse motivo, é recomendável que os tipos que sobrecarregam os operadores incluam um método secundário com um nome específico de domínio apropriado que forneça funcionalidade equivalente.

Especificamente, F # é uma das linguagens que torna difícil invocar operadores de conversão implícitos.


Além disso, não acho que ter apenas uma maneira de fazer as coisas seja tão importante. Na minha opinião, é mais importante tornar a API conveniente. Se eu apenas quiser combinar códigos hash de poucos valores, acho que HashCode.CombineHashCodes(value1, value2, value3) é mais simples, curto e fácil de entender do que new HashCode().Combine(value1).Combine(value2).Combine(value3) .

A API do método de instância ainda é útil para casos mais complicados, mas acho que o caso mais comum deveria ter a API de método estático mais simples.

@svick , sua oferecem suporte a operadores tão bem é legítima. Eu rendo, vamos adicionar Value então.

Não acho que ter apenas uma maneira de fazer as coisas seja tão importante.

É importante. Se alguém faz isso de uma maneira e lê o código de uma pessoa que faz de outra, então ele / ela terá que pesquisar no Google o que a outra forma faz.

Se eu só quiser combinar códigos hash de poucos valores, acho que HashCode.CombineHashCodes (valor1, valor2, valor3) é mais simples, curto e fácil de entender do que o novo HashCode (). Combine (valor1) .Combine (valor2) .Combine ( valor3).

  • O problema com um método estático é que, como não haverá sobrecarga de params int[] , teremos que adicionar sobrecargas para cada aridade diferente, o que é muito menos custo-benefício. É muito mais agradável ter um método que cubra todos os casos de uso.
  • A segunda forma será fácil de entender, uma vez que você a veja uma ou duas vezes. Na verdade, você poderia argumentar que é mais legível, já que é mais fácil encadear verticalmente (e, portanto, minimiza as diferenças quando um campo é adicionado / removido):
public override int GetHashCode()
{
    return new HashCode()
        .Combine(_field1)
        .Combine(_field2)
        .Combine(_field3)
        .Combine(_field4);
}

[@svick] Não acho que ter apenas uma maneira de fazer as coisas seja tão importante.

Acho que minimizar o número de maneiras pelas quais você pode fazer a mesma coisa é importante porque evita confusão. Ao mesmo tempo, nossa meta não é ser 100% livre de sobreposições se isso ajudar a realizar outras metas, como descoberta, conveniência, desempenho ou legibilidade. Em geral, nosso objetivo é minimizar conceitos, em vez de APIs. Por exemplo, várias sobrecargas são menos problemáticas do que ter vários métodos diferentes com terminologia separada.

A razão pela qual adicionei o método de fábrica é para deixar claro como se obtém um código hash inicial. Criar a estrutura vazia seguida por Combine não parece muito intuitivo. O lógico seria adicionar .ctor, mas para evitar boxing teria que ser genérico, o que você não pode fazer com um .ctor. Um método de fábrica genérico é a segunda melhor opção.

Um bom efeito colateral é que ele se parece muito com a aparência das estruturas de dados imutáveis ​​na estrutura. E no design da API, favorecemos fortemente a consistência em relação a quase qualquer outra coisa.

[@svick] Se eu apenas quiser combinar códigos hash de poucos valores, acho que HashCode.CombineHashCodes (valor1, valor2, valor3) é mais simples, curto e fácil de entender do que new HashCode (). Combine (valor1) .Combine (valor2 ). Combinar (valor 3).

Eu concordo com @jamesqo : o que eu gosto no padrão do construtor é que ele é dimensionado para uma quantidade arbitrária de argumentos com penalidade de desempenho mínima (se houver, dependendo de quão bom é o nosso inliner).

[@jamesqo] Não acho que a sobrecarga de aceitar uma string / StringComparison pertença aqui; HashCode não tem nada a ver com strings

Ponto justo. Eu o adicionei porque ele foi referenciado no código de @Eilon . Por experiência própria, eu diria que cordas são supercomuns. Por outro lado, não tenho certeza se especificar uma comparação seja. Vamos deixar isso de lado por enquanto.

[@jamesqo] Temos que mover a longa sobrecarga para um novo tipo. HashCode terá apenas 32 bits de largura; não cabe muito tempo.

Este é um bom ponto. Precisamos mesmo de uma versão long ? Eu só deixei porque foi mencionado acima e eu realmente não pensei sobre isso.

Agora que estou, parece que devemos deixar apenas 32 bits, porque é disso que trata o .NET GetHashCode() . Nesse sentido, não tenho certeza se devemos adicionar a versão uint . Se você usar hash fora desse reino, acho que não há problema em apontar as pessoas para os algoritmos de hash de propósito mais geral que temos em System.Security.Cryptography .

`` `C #
public struct HashCode
{
public static HashCode Create(T obj);

[Pure] public HashCode Combine(int hashCode);
[Pure] public HashCode Combine<T>(T obj);

public int Value { get; }

public static implicit operator int(HashCode hashCode);

}
`` `

Agora que estou, parece que devemos deixar apenas 32 bits, porque é disso que trata o .NET GetHashCode (). Nesse sentido, nem tenho certeza se devemos adicionar a versão uint. Se você usar hash fora desse domínio, acho que não há problema em apontar as pessoas para os algoritmos de hash de propósito mais geral que temos em System.Security.Cryptography.

@terrajobst Existem tipos muito diferentes de algoritmos de hash, um verdadeiro zoológico. Na verdade, provavelmente 70% são não criptográficos por design. E provavelmente mais da metade deles são projetados para lidar com mais de 64 bits (o alvo comum é 128/256). Aposto que o framework decidiu usar 32 bits (não estive lá) porque na época o x86 ainda era um grande consumidor e os hashes são usados ​​em todos os lugares, então o desempenho em um hardware menor era fundamental.

Além disso, para serem estritas, a maioria das funções hash são realmente definidas no domínio uint , e não no int porque as regras de deslocamento são diferentes. Na verdade, se você verificar o código que postei antes, o int é imediatamente convertido em uint por causa disso (e use a otimização ror/rol ). Aponte no caso, se quisermos ser estritos, o único hash deve ser uint , pode ser visto como um descuido que o framework retorna int sob essa luz.

Restringir isso a int não é melhor do que o que temos hoje. Se fosse minha chamada, eu pressionaria a equipe de design para ver como poderíamos acomodar o suporte de 128 e 256 variantes e diferentes funções de hash (mesmo se jogássemos uma alternativa não me faça pensar sob suas impressões digitais).

Os problemas causados ​​pela simplificação excessiva às vezes são piores do que os problemas de design introduzidos quando forçados a lidar com coisas complexas. Simplificar a funcionalidade a um grau tão grande porque os desenvolvedores são percebidos como not being able to deal with having multiple options pode facilmente levar ao caminho do estado atual do SIMD. A maioria dos desenvolvedores preocupados com o desempenho não pode usá-lo, e bem, todos os outros também não o usarão porque a maioria não está lidando com aplicativos sensíveis ao desempenho que têm metas de rendimento tão finas de qualquer maneira.

O caso de hashing é semelhante, os domínios onde você usaria 32 bits são muito restritos (a maioria já está coberta pelo próprio framework), para o resto você está sem sorte.

image

Além disso, assim que você tiver que lidar com mais de 75.000 elementos, terá 50% de chance de haver uma colisão, o que é ruim na maioria dos cenários (e isso presumindo que você tenha uma função hash bem projetada). É por isso que 64 bits e 128 bits são tão usados ​​fora dos limites das estruturas de tempo de execução.

Com um design preso em int , estamos apenas cobrindo os problemas causados ​​por não termos o jornal de segunda-feira em 2000 (então agora todo mundo escreve seu pobre hash sozinho), mas não avançaremos nem por um passo o estado do arte também.

São meus 2 centavos na discussão.

@redknightlois , acho que entendemos as limitações dos hashes internos. Mas eu concordo com @terrajobst : esse recurso deve ser sobre APIs para calcular hashes com a finalidade de retorná-los de substituições Object.GetHashCode. Além disso, podemos ter uma biblioteca separada para um hashing mais moderno, mas eu diria que deve ser uma discussão separada, pois precisa incluir a decisão sobre o que fazer com Object.GetHashCode e todas as estruturas de dados de hash existentes.

A menos que você ache que ainda é benéfico fazer a combinação de hash em 128 bits e, em seguida, converter para int para que o resultado possa ser retornado de GetHahsCode.

@KrzysztofCwalina Eu concordo que são duas abordagens diferentes. Uma é corrigir um problema causado em 2000; outra é lidar com o problema geral de hashing. Se todos concordarmos que esta é uma solução para o primeiro, a discussão acabou. No entanto, para uma discussão de design para um marco "Futuro", tenho a sensação de que ficará aquém, principalmente porque o que faremos aqui terá impacto na discussão futura. Cometer erros aqui terá um impacto.

@redknightlois , eu proporia o seguinte: vamos projetar uma API como se não

@redknightlois

Cometer erros aqui terá um impacto.

Acho que se, no futuro, quisermos oferecer suporte a cenários mais avançados, podemos apenas fazer isso em um tipo separado de HashCode . As decisões aqui não devem realmente impactar esses casos.

Eu criei um problema diferente para começar a lidar com isso.

@redknightlois : +1 :. A propósito, você respondeu antes que eu pudesse editar meu comentário, mas eu realmente experimentei sua ideia (acima) de fazer o hash funcionar com qualquer tipo (int, long, decimal, etc.) e encapsular a lógica de hash central em uma estrutura: https://github.com/jamesqo/HashApi (exemplo de uso aqui ). Mas, ter dois parâmetros de tipo genérico acabou sendo muito complexo, e a inferência de tipo do compilador acabou não funcionando quando tentei usar a API. Então, sim, é uma boa ideia fazer um hash mais avançado em um problema separado por enquanto.

@terrajobst A API parece quase pronta, mas há mais 1 ou 2 coisas que gostaria de mudar.

  • Inicialmente, eu não queria o método de fábrica estático, já que HashCode.Create(x) tem o mesmo efeito que new HashCode().Combine(x) . Mas mudei de ideia sobre isso, pois isso significa 1 hash extra. Em vez disso, por que não renomeamos Create para Combine ? Parece meio chato ter que digitar uma coisa para o primeiro campo e outra para o segundo campo.
  • Acho que devemos fazer HashCode implementar IEquatable<HashCode> e implementar alguns dos operadores de igualdade. Sinta-se à vontade para me informar se tiver alguma objeção.

(Esperançosamente) proposta final:

public struct HashCode : IEquatable<HashCode>
{
    public static HashCode Combine(int hash);
    public static HashCode Combine<T>(T obj);

    public HashCode Combine(int hash);
    public HashCode Combine<T>(T obj);

    public int Value { get; }

    public static implicit operator int(HashCode hashCode);

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

    public override bool Equals(object obj);
    public override bool Equals(HashCode other);
    public override int GetHashCode();
}

// Usage:

public override int GetHashCode()
{
    return HashCode
        .Combine(_field1)
        .Combine(_field2)
        .Combine(_field3)
        .Combine(_field4);
}

@terrajobst disse:

Ponto justo. Eu o adicionei porque ele foi referenciado no código de @Eilon . Por experiência própria, eu diria que cordas são supercomuns. Por outro lado, não tenho certeza se especificar uma comparação seja. Vamos deixar isso de lado por enquanto.

Na verdade, é superimportante: a criação de hashes para strings geralmente envolve levar em consideração o propósito dessa string, o que envolve tanto sua cultura quanto sua distinção entre maiúsculas e minúsculas. O StringComparer não trata de comparações em si, mas sim de fornecer implementações GetHashCode específicas que reconhecem a cultura / maiúsculas e minúsculas.

Sem esta API, você precisaria fazer algo estranho como:

HashCode.Combine(str1.ToLowerInvariant()).Combine(str2.ToLowerInvariant())

E isso está repleto de alocações, segue padrões de baixa sensibilidade à cultura, etc.

@Eilon , nesse caso, esperaria que o código explicitamente chamasse string.GetHashCode(StringComparison comparison) que reconhece maiúsculas e minúsculas, e passe o resultado como int para Combine .

c# HashCode.Combine(str1.GetHashCode(StringComparer.Ordinal)).Combine(...)

@Eilon , você poderia apenas usar StringComparer.InvariantCultureIgnoreCase.GetHashCode.

Essas são certamente melhores em termos de alocações, mas essas chamadas não são bonitas de se olhar ... Nós usamos all em todo o ASP.NET onde os hashes precisam incluir strings de cultura / maiúsculas e minúsculas.

Muito justo, combinando tudo o que foi dito acima, que tal esta forma então:

`` `C #
namespace System.Collections.Generic
{
public struct HashCode: IEquatable
{
Combinar HashCode estático público (hash int);
public static HashCode Combine(T obj);
Combinar HashCode estático público (texto de string, comparação de StringComparison);

    public HashCode Combine(int hash);
    public HashCode Combine<T>(T obj);
    public HashCode Combine(string text, StringComparison comparison);

    public int Value { get; }

    public static implicit operator int(HashCode hashCode);

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

    public override bool Equals(object obj);
    public override bool Equals(HashCode other);
    public override int GetHashCode();
}

}

// Uso:

public override int GetHashCode ()
{
return HashCode.Combine (_field1)
.Combine (_field2)
.Combine (_field3)
.Combine (_field4);
}
`` `

enviá-lo! :-)

@terrajobst _Hold on --_ não pode Combine(string, StringComparison) ser implementado apenas como um método de extensão?

public static class HashCodeExtensions
{
    public static HashCode Combine(this HashCode hashCode, string text, StringComparison comparison)
    {
        switch (comparison)
        {
            case StringComparison.Ordinal:
                return HashCode.Combine(StringComparer.Ordinal.GetHashCode(text));
            case StringComparison.OrdinalIgnoreCase:
                ...
        }
    }
}

Eu preferiria muito, muito mais que fosse um método de extensão em vez de uma parte da assinatura de tipo. No entanto, se você ou @Elion realmente acharem que esse deve ser um método integrado, não bloquearei esta proposta.

( editar: também System.Numerics é provavelmente um namespace melhor, a menos que tenhamos tipos relacionados a hash em Collections.Generic hoje que eu não conheço.)

LGTM. Eu iria extensão.

Sim, poderia ser um método de extensão, mas que problema ele resolve?

@terrajobst

Sim, poderia ser um método de extensão, mas que problema ele resolve?

Eu estava sugerindo em código ASP.NET. Se for comum para o seu caso de uso, tudo bem, mas pode não ser verdade para outras bibliotecas / aplicativos. Se for descoberto que isso é comum posteriormente, podemos sempre reavaliar e decidir adicioná-lo em uma proposta separada.

Mhhh este é o núcleo de qualquer maneira. Uma vez definido, ele fará parte da assinatura de qualquer maneira. Desfaça-se do comentário. Está tudo bem como está.

Usar métodos de extensão é útil para casos em que:

  1. é um tipo existente que gostaríamos de aumentar sem ter que enviar uma atualização para o tipo em si
  2. resolver problemas de camadas
  3. separar APIs supercomuns de APIs muito menos usadas.

Não acho que (1) ou (2) se apliquem aqui. (3) só ajudaria se movêssemos o código para um assembly diferente de HashCode ou se movêssemos para um namespace diferente. Eu diria que as cordas são comuns o suficiente para não valer a pena. Na verdade, eu até diria que eles são tão comuns que tratá-los como de primeira classe faz mais sentido do que tentar separá-los artificialmente em um tipo de extensão.

@terrajobst , para ficar claro, eu estava sugerindo abandonar a API string completo e deixar para o ASP.NET escrever seu próprio método de extensão para strings.

Eu diria que as cordas são comuns o suficiente para não valer a pena. Na verdade, eu até diria que eles são tão comuns que tratá-los como de primeira classe faz mais sentido do que tentar separá-los artificialmente em um tipo de extensão.

Sim, mas quão comum é alguém querer obter o código hash não ordinal de uma string, que é o único cenário que a sobrecarga de Combine<T> não cuida? (por exemplo, alguém que chama StringComparer.CurrentCulture.GetHashCode em suas substituições?) Posso estar errado, mas não vi muitos.

Desculpe pela resistência sobre isso; acontece que, uma vez que uma API é adicionada, não há como voltar atrás.

sim, mas quão comum é alguém querer obter o código hash não ordinal de uma string

Posso ser tendencioso, mas a invariância de maiúsculas e minúsculas é bastante popular. Claro, não muitos (se houver) se importam com códigos hash específicos da cultura, mas códigos hash que ignoram maiúsculas e minúsculas eu posso ver totalmente - e parece que @Eilon está atrás (que é StringComparison.OrdinalIgnoreCase ).

Desculpe pela resistência sobre isso; acontece que, uma vez que uma API é adicionada, não há como voltar atrás.

Sem brincadeira 😈 Concordo, mas mesmo que a API não seja tão usada, é útil e não causa nenhum dano.

@terrajobst Ok então, vamos adicionar: +1: Último problema: eu mencionei isso acima, mas podemos fazer o namespace Numerics em vez de Collections.Generic? Se tivéssemos que adicionar mais tipos relacionados a hashing no futuro, como @redknightlois sugere, acho que seria uma

Eu estou adorando. 🍔

Não acho que o Hashing se enquadre conceitualmente nas Coleções. E sobre System.Runtime?

Eu ia sugerir o mesmo, ou mesmo System. Também não é numérico.

@karelz , System.Runtime pode funcionar. @redknightlois System seria conveniente, uma vez que provavelmente você já importou esse namespace. Não sei se isso seria o apropriado (novamente, se mais tipos de hash forem adicionados).

Não devemos colocá-lo em System.Runtime pois isso é para casos esotéricos e bastante especializados. Falei com @KrzysztofCwalina e ambos achamos que é um dos dois:

  • System
  • System.Collections.*

Ambos nos inclinamos para System .

Se o que precisamos é uma justificativa de por que ir para System , posso tentar uma justificativa. Construímos HashCode para ajudar nas implementações de object.GetHashCode() , parece adequado que ambos compartilhem o namespace.

@terrajobst Acho que System deveria ser o namespace, então. Vamos: enviar:

Atualizada a especificação da API na descrição.

[@redknightlois] Se o que precisamos é uma justificativa de por que ir para System , posso tentar uma justificativa. Construímos HashCode para ajudar nas implementações de object.GetHashCode() , parece adequado que ambos compartilhem o namespace.

Esse foi o raciocínio que @KrzysztofCwalina e eu também usamos. Vendido!

@jamesqo

Presumo que você também queira fornecer a implementação ao PR?

@terrajobst Sim, definitivamente. Obrigado por reservar um tempo para revisar isso!

Sim definitivamente.

Doce. Nesse caso, vou deixar atribuído a você. Isso é bom para você @karelz?

Obrigado por reservar um tempo para revisar isso!

Obrigado por dedicar seu tempo trabalhando conosco no formato da API. Pode ser um processo doloroso ir e vir. Agradecemos muito sua paciência!

E estou ansioso para excluir a implementação do ASP.NET Core e usá-la em seu lugar 😄

Combinar HashCode estático público (texto de string, comparação de StringComparison);
public HashCode Combine (texto da string, comparação StringComparison);

Nit: Os métodos em String que levam StringComparison (por exemplo, Equals , Compare , StartsWith , EndsWith , etc. .) use comparisonType como o nome do parâmetro, não comparison . O parâmetro deve ser nomeado comparisonType aqui também para ser consistente?

@justinvp , que parece mais uma falha de nomenclatura nos métodos de String; Type é redundante. Não acho que devamos tornar os nomes dos parâmetros em novas APIs mais prolixos apenas para "seguir o precedente" com os antigos.

Como outro ponto de dados, xUnit escolheu usar comparisonType também.

@justinvp Você me convenceu. Agora que penso sobre isso intuitivamente, "não faz distinção entre maiúsculas e minúsculas" ou "dependente da cultura" é um 'tipo' de comparação. Vou mudar o nome.

Estou ok com o formato disso, mas em relação ao StringComparison, uma alternativa possível:

Não inclua:

`` `C #
Combinar HashCode estático público (texto de string, comparação de StringComparison);
public HashCode Combine (texto da string, comparação StringComparison);

Instead, add a method:

``` C#
public class StringComparer
{
    public static StringComparer FromComparison(StringComparison comparison);
    ...
}

Então, em vez de escrever:

`` `C #
public override int GetHashCode ()
{
return HashCode.Combine (_field1)
.Combine (_field2)
.Combine (_field3)
.Combine (_field4, _comparison);
}

you write:

``` C#
public override int GetHashCode()
{
    return HashCode.Combine(_field1)
                   .Combine(_field2)
                   .Combine(_field3)
                   .Combine(StringComparer.FromComparison(_comparison).GetHashCode(_field4));
}

Sim, é um pouco mais longo, mas resolve o mesmo problema sem precisar de dois métodos especializados em HashCode (que acabamos de promover a System), e você obtém um método auxiliar estático que pode ser usado em outras situações não relacionadas. Ele também o mantém semelhante ao de como você o usaria se já tiver um StringComparer (uma vez que não estamos falando sobre sobrecargas de comparador):

C# public override int GetHashCode() { return HashCode.Combine(_field1) .Combine(_field2) .Combine(_field3) .Combine(_comparer.GetHashCode(_field4)); }

@stephentoub , FromComparison parece uma boa ideia. Na verdade, propus para cima no encadeamento adicionar uma string.GetHashCode(StringComparison) api, o que torna seu exemplo ainda mais simples (assumindo uma string não nula):

public override int GetHashCode()
{
    return HashCode.Combine(_field1)
                   .Combine(_field2)
                   .Combine(_field3)
                   .Combine(_field4.GetHashCode(_comparison));
}

@Elion disse que adicionou muitas ligações.

(editar: fez uma proposta para sua api.)

Eu também não gosto de adicionar 2 métodos especializados em HashCode para string.
@Eilon, você mencionou que o padrão é usado no próprio ASP.NET Core. Quanto você acha que os desenvolvedores externos irão usá-lo?

@jamesqo obrigado por conduzir o design! Como @terrajobst disse, agradecemos sua ajuda e paciência. Às vezes, pequenas APIs fundamentais podem demorar um pouco para serem iteradas em :).

Vamos ver onde chegamos com este último feedback da API, então podemos prosseguir com a implementação.

Deve haver um:

C# public static HashCode Combine<T>(T obj, IEqualityComparer<T> cmp);

?

(Peço desculpas se isso já foi dispensado e estou perdendo isso aqui).

@stephentoub disse:

escrever:

c# public override int GetHashCode() { return HashCode.Combine(_field1) .Combine(_field2) .Combine(_field3) .Combine(StringComparer.FromComparison(_comparison).GetHashCode(_field4)); }

Sim, é um pouco mais longo, mas resolve o mesmo problema sem precisar de dois métodos especializados em HashCode (que acabamos de promover a System), e você obtém um método auxiliar estático que pode ser usado em outras situações não relacionadas. Ele também o mantém semelhante ao de como você o usaria se já tiver um StringComparer (uma vez que não estamos falando sobre sobrecargas de comparador):


Bem, não é apenas um pouco mais longo, é tipo muuuito mais longo e tem zero de descoberta.

Qual é a resistência em adicionar este método? Se for útil, pode ser implementado de forma clara e correta, não tem ambigüidade no que faz, por que não adicioná-lo?

Ter o método auxiliar / de conversão estático adicional é bom - embora eu não tenha certeza se o usaria - mas por que às custas de métodos de conveniência?

por que às custas de métodos de conveniência?

Porque não está claro para mim os métodos de conveniência são realmente necessários aqui. Eu percebi que o ASP.NET faz isso em vários lugares. Quantos lugares? E em quantos desses lugares é realmente uma variável StringComparison que você tem em vez de um valor conhecido? Nesse caso, você nem mesmo precisa do ajudante que mencionei e poderia apenas fazer:

`` `C #
.Combine (StringComparer.InvariantCulture.GetHashCode (_field4))

which in no way seems onerous to me or any more undiscoverable than knowing about StringComparison and doing:

``` C#
.Combine(_field4, StringComparison.InvariantCulture);

e é realmente mais rápido, já que não temos que ramificar dentro do Combine para fazer exatamente a mesma coisa que o dev poderia ter escrito. O código extra é tão inconveniente que vale a pena adicionar sobrecargas especializadas para aquele caso? Por que não sobrecarrega para StringComparer? Por que não sobrecarrega para EqualityComparer? Por que não sobrecargas que demoram Func<T, int> ? Em algum ponto você traça o limite e diz "o valor que essa sobrecarga fornece simplesmente não vale a pena", porque tudo o que adicionamos tem um custo, seja o custo de manutenção, o custo do tamanho do código, o custo de qualquer coisa , e se o desenvolvedor realmente precisar desse caso, é muito pouco código adicional para o desenvolvedor lidar com menos casos especializados. Então, eu estava sugerindo que talvez o lugar certo para traçar a linha seja antes dessas sobrecargas, e não depois (mas como afirmei no início da minha resposta anterior, "Estou bem com o formato disso", e estava sugerindo uma alternativa) .

Aqui está a pesquisa que fiz: https://github.com/search?p=2&q=user%3Aaspnet+hashcodecombiner&type=Code&utf8=%E2%9C%93

De cerca de 100 correspondências, mesmo nas primeiras páginas, quase todos os casos de uso têm strings e, em vários casos, usam diferentes tipos de comparações de strings:

  1. Ordinal: https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/TagHelperAttributecscriptor#Descriptor#Desp.
  2. Ordinal + IgnoreCase: https://github.com/aspnet/Razor/blob/bdbb854bdbde260b3c70f565a93ebbb185a7c5a7/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagHelperCompareributeRequired49
  3. Ordinal: https://github.com/aspnet/Razor/blob/bdbb854bdbde260b3c70f565a93ebbb185a7c5a7/src/Microsoft.AspNetCore.Razor/Chunks/Generators/AttributeBlockChunkGenerator.cs#L58
  4. Ordinal: https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/TagHelperDesignTimeDescriptorComparer.
  5. Ordinal: https://github.com/aspnet/Razor/blob/dbcb6901209859e471c9aa978912cf7d6c178668/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/AttributeBlockChunkGenerator.cs#L56
  6. Ordinal: https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/CaseSensitiveTagHelper62
  7. Ordinal + IgnoreCase: https://github.com/aspnet/dnx/blob/bebc991012fe633ecac69675b2e892f568b927a5/src/Microsoft.Dnx.Tooling/NuGet/Core/PackageSource/PackageSource.cs#L107
  8. Ordinal: https://github.com/aspnet/Razor/blob/bdbb854bdbde260b3c70f565a93ebbb185a7c5a7/src/Microsoft.AspNetCore.Razor/Tokenizer/Symbol/SymbolBase.cs#L52
  9. Ordinal: https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/CaseSensitiveTagHelper39
  10. Ordinal: https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/TagHelperAttribute42TimeDesign

(E dezenas de outros.)

Portanto, parece que certamente dentro da base de código ASP.NET Core este é um padrão extremamente comum. Claro que não posso falar com nenhum outro sistema.

De cerca de 100 correspondências

Cada um dos 10 que você listou (não olhei para o resto da pesquisa) especifica explicitamente a comparação de string, em vez de extraí-la de uma variável, então não estamos apenas falando sobre a diferença entre, por exemplo:

`` `C #
.Combine (Name, StringComparison.OrdinalIgnoreCase)

``` C#
.Combine(StringComparer.OrdinalIgnoreCase.GetHashCode(Name))

? Isso não é "muuuito mais longo" e é mais eficiente, a menos que esteja faltando alguma coisa.

De qualquer forma, como afirmei, estou apenas sugerindo que realmente consideremos se essas sobrecargas são necessárias. Se a maioria das pessoas acredita que sim, e não estamos apenas considerando nossa própria base de código ASP.NET, tudo bem.

Relacionado, qual é o comportamento que estamos planejando para entradas nulas? E quanto a int == 0? Posso começar a ver mais benefícios para a sobrecarga de string se permitirmos que null seja passado, pois acredito que StringComparer.GetHashCode normalmente gera uma entrada nula, então, se isso for realmente comum, começa a se tornar mais complicado se o chamador tiver para casos especiais nulos. Mas isso também levanta a questão de qual será o comportamento quando nulo for fornecido. Um 0 está misturado ao código hash como com qualquer outro valor? É tratado como um nop e o hashcode é deixado sozinho?

Acho que a melhor abordagem geral para nulo é misturar em um zero. Para um único elemento nulo adicionado, tê-lo como nop seria melhor, mas se alguém estiver alimentando em uma sequência, torna-se mais benéfico ter um hash de 10 nulos diferente de 20.

Na verdade, meu voto vem da perspectiva da base de código do ASP.NET Core, em que seria muito útil ter uma sobrecarga com reconhecimento de string. As coisas sobre o comprimento da linha não eram realmente minha principal preocupação, mas sim sobre a descoberta.

Se uma sobrecarga com reconhecimento de string não estivesse disponível no sistema, apenas adicionaríamos um método de extensão interno no ASP.NET Core e usá-lo-íamos.

Se uma sobrecarga com reconhecimento de string não estivesse disponível no sistema, apenas adicionaríamos um método de extensão interno no ASP.NET Core e usá-lo-íamos.

Acho que seria uma ótima solução por enquanto, até que vejamos mais evidências de que essa API é necessária em geral, também fora da base de código do ASP.NET Core.

Devo dizer que não vejo valor em remover a sobrecarga de string . Não reduz a complexidade, não torna o código mais eficiente e não nos impede de melhorar outras áreas, como fornecer um método que retorna StringComparer de StringComparison . Açúcar sintático _faz_ importa, porque .NET sempre se preocupou em tornar o caso comum mais fácil. Também queremos orientar o desenvolvedor a fazer a coisa certa e cair no poço do sucesso.

Precisamos reconhecer que as cordas são especiais e incrivelmente comuns. Ao adicionar uma sobrecarga que os especializa, alcançamos duas coisas:

  1. Tornamos cenários como o do @Eilon muito mais fáceis.
  2. Tornamos detectável que considerar a comparação de cordas é importante, especialmente o revestimento.

Também precisamos considerar que os auxiliares padronizados comuns, como o método de extensão

No entanto, se a principal preocupação for sobre o invólucro especial string , que tal:

`` `C #
public struct HashCode: IEquatable
{
public HashCode Combine(T obj, IEqualityComparercomparador);
}

// Uso
return HashCode.Combine (_numberField)
.Combine (_stringField, StringComparer.OrdinalIgnoreCase);
`` `

@terrajobst , seu compromisso é inteligente. Gosto de como você não precisa mais chamar GetHashCode explicitamente ou aninhar um conjunto extra de parênteses com um comparador personalizado.

(editar: eu acho que realmente devo creditar isso a @JonHanna, já que ele mencionou isso antes no tópico? 😄)

@JonHanna Sim, também faremos hash de entradas nulas como 0.

Desculpe por interromper a conversa aqui. Mas, onde devo colocar o novo tipo? @mellinoe @ericstj @weshaggard , você sugere que eu faça um novo conjunto / pacote para este tipo como System.HashCode , ou devo adicioná-lo a um conjunto existente como System.Runtime.Extensions ? Obrigado.

Recentemente, refatoramos bastante o layout do assembly no .NET Core; Sugiro colocá-lo onde vivem os comparadores concretos, que parecem indicar System.Runtime.Extensions .

@weshaggard?

@terrajobst Com relação à proposta em si, acabei de descobrir que não podemos nomear as sobrecargas estáticas e de instância Combine , infelizmente. 😢

O seguinte resulta em um erro do compilador porque os métodos de instância e estáticos não podem ter os mesmos nomes:

using System;
using System.Collections.Generic;

public struct HashCode
{
    public void Combine(int i)
    {
    }

    public static void Combine(int i)
    {
    }
}

Agora temos 2 opções:

  • Renomeie as sobrecargas estáticas para algo diferente, como Create , Seed , etc.
  • Mova as sobrecargas estáticas para outra classe estática:
public static class Hash
{
    public static HashCode Combine(int hash);
}

public struct HashCode
{
    public HashCode Combine(int hash);
}

// Usage:
return Hash.Combine(_field1)
           .Combine(_field2)
           .Combine(_field3);

Sou preferencial para o segundo. É uma pena que temos que contornar esse problema, mas ... pensamentos?

Separar a lógica em 2 tipos parece estranho para mim - para usar HashCode você tem que fazer a conexão e começar com a classe Hash .

Prefiro adicionar o método Create (ou Seed ou Init ).
Eu adicionaria também sobrecarga sem argumentos HashCode.Create().Combine(_field1).Combine(_field2) .

@karelz , não acho que devemos adicionar um método de fábrica se não for o mesmo nome. Devemos apenas oferecer o construtor sem parâmetros, new , já que é mais natural. Além disso, não podemos evitar que as pessoas escrevam new HashCode().Combine pois é uma estrutura.

public override int GetHashCode()
{
    return new HashCode()
        .Combine(_field1)
        ...
}

Isso faz uma combinação extra com o código hash 0 e _field1 , em vez de inicializar diretamente a partir do código hash. No entanto, um efeito colateral do hash atual que estamos usando é que 0 é passado como o primeiro parâmetro, ele será girado para zero e adicionado a zero. E quando 0 é corrigido com o primeiro código hash, ele apenas produzirá o primeiro código hash. Portanto, se o JIT for bom em dobramento constante (e eu acredito que ele otimiza isso xou eliminá-lo), na verdade isso deve ser equivalente à inicialização direta.

API proposta (especificação atualizada):

namespace System
{
    public struct HashCode : IEquatable<HashCode>
    {
        public HashCode Combine(int hash);
        public HashCode Combine<T>(T obj);
        public HashCode Combine<T>(T obj, IEqualityComparer<T> comparer);

        public int Value { get; }

        public static implicit operator int(HashCode hashCode);

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

        public override bool Equals(object obj);
        public override bool Equals(HashCode other);
        public override int GetHashCode();
    }
}

@redknightlois @JonHanna @stephentoub @Eilon , você tem uma opinião sobre um método de fábrica versus o uso do construtor padrão? Descobri que o compilador não permite uma sobrecarga estática de Combine pois isso entra em conflito com os métodos de instância, então temos a opção de qualquer um

HashCode.Create(field1).Combine(field2) // ...

// or, using default constructor

new HashCode().Combine(field1).Combine(field2) // ...

A vantagem do primeiro é que é um pouco mais conciso. A vantagem do segundo é que ele terá uma nomenclatura consistente para que você não precise escrever algo diferente para o primeiro campo.

Outra possibilidade são dois tipos diferentes, um com a fábrica Combine , um com a instância Combine (ou o segundo como uma extensão do primeiro tipo).

Não tenho certeza de qual prefiro TBH.

@JonHanna , sua segunda ideia com as sobrecargas de instância sendo métodos de extensão parece ótima. Dito isso, hc.Combine(obj) nesse caso tenta pegar a sobrecarga estática: TryRoslyn .

Propus ter uma classe estática como ponto de entrada alguns comentários acima, o que me lembra ... @karelz , você disse

Separar a lógica em 2 tipos parece estranho para mim - para usar o HashCode, você precisa fazer a conexão e começar com a classe Hash.

Que conexão as pessoas teriam que fazer? Não deveríamos apresentá-los a Hash primeiro e, a partir daí, eles podem seguir seu caminho para HashCode ? Não acho que adicionar uma nova classe estática seria um problema.

Separar a lógica em 2 tipos parece estranho para mim - para usar o HashCode, você precisa fazer a conexão e começar com a classe Hash.

Poderíamos manter o tipo de nível superior HashCode e apenas aninhar a estrutura. Isso permitiria o uso desejado, mantendo o "ponto de entrada" da API em um tipo de nível superior, por exemplo:

`` `c #
sistema de namespace
{
public static class HashCode
{
HashCodeValue Combine estático público (int hash);
public static HashCodeValue Combine(T obj);
public static HashCodeValue Combine(T obj, IEqualityComparercomparador);

    public struct HashCodeValue : IEquatable<HashCodeValue>
    {
        public HashCodeValue Combine(int hash);
        public HashCodeValue Combine<T>(T obj);
        public HashCodeValue Combine<T>(T obj, IEqualityComparer<T> comparer);

        public int Value { get; }

        public static implicit operator int(HashCodeValue hashCode);

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

        public bool Equals(HashCodeValue other);
        public override bool Equals(object obj);
        public override int GetHashCode();
    }
}

}
`` `

Edit: Embora, provavelmente precise de um nome melhor do que HashCodeValue para o tipo aninhado se seguirmos este caminho, pois HashCodeValue.Value é um pouco redundante, não que Value seria muito usado frequentemente. Talvez nem precisemos de uma propriedade Value - você pode obter Value por meio de GetHashCode() se não quiser lançar em int .

@justinvp Em primeiro lugar, qual é o problema de ter dois tipos separados? Este sistema parece funcionar bem para LinkedList<T> e LinkedListNode<T> , por exemplo.

Mas qual é o problema de ter dois tipos separados em primeiro lugar?

Existem duas preocupações com os dois tipos de nível superior:

  1. Qual tipo é o "ponto de entrada" para a API? Se os nomes forem Hash e HashCode , com qual deles você começa? Não está claro a partir desses nomes. Com LinkedList<T> e LinkedListNode<T> é bastante claro qual é o ponto de entrada principal, LinkedList<T> , e qual é um ajudante.
  2. Poluindo o namespace System . Não é tão preocupante quanto (1), mas é algo a ter em mente ao considerarmos a exposição de novas funcionalidades no namespace System .

O aninhamento ajuda a mitigar essas preocupações.

@justinvp

Qual tipo é o "ponto de entrada" para a API? Se os nomes forem Hash e HashCode, com qual você começa? Não está claro a partir desses nomes. Com LinkedListe LinkedListNodeestá bem claro qual é o ponto de entrada principal, LinkedList, e que é um ajudante.

OK, ponto bastante justo. E se nomearmos os tipos Hash e HashValue , e não tipos de aninhamento? Isso denotaria o suficiente de uma relação de subjugação entre os dois tipos?

Se fizermos isso, o método de fábrica se tornará ainda mais conciso: Hash.Combine(field1).Combine(field2) . Além disso, usar o tipo de estrutura em si ainda é prático. Por exemplo, alguém pode querer coletar uma lista de hashes e comunicar isso ao leitor, um List<HashValue> é usado em vez de um List<int> . Isso pode não funcionar tão bem se fizermos o tipo aninhado: List<HashCode.HashCodeValue> (mesmo List<Hash.Value> é meio confuso à primeira vista).

Poluindo o namespace do sistema. Não é tão preocupante quanto (1), mas é algo a ter em mente ao considerarmos a exposição de novas funcionalidades no namespace System.

Eu concordo, mas também acho importante seguirmos as convenções e não sacrificarmos a facilidade de uso. Por exemplo, as únicas APIs BCL que eu consigo pensar em que temos tipos aninhados (coleções imutáveis ​​não contam, elas não são estritamente parte da estrutura) é List<T>.Enumerator , onde queremos ativamente ocultar os aninhados type porque se destina ao uso do compilador. Não queremos fazer isso neste caso.

Talvez nem precisemos de uma propriedade Value - você pode obter o valor por meio de GetHashCode () se não quiser converter em int.

Eu pensei sobre isso antes. Mas então como o usuário vai saber que o tipo sobrescreve GetHashCode , ou que tem um operador implícito?

API proposta

public static class Hash
{
    public static HashValue Combine(int hash);
    public static HashValue Combine<T>(T obj);
    public static HashValue Combine<T>(T obj, IEqualityComparer<T> comparer);
}

public struct HashValue : IEquatable<HashValue>
{
    public HashValue Combine(int hash);
    public HashValue Combine<T>(T obj);
    public HashValue Combine<T>(T obj, IEqualityComparer<T> comparer);

    public int Value { get; }

    public static implicit operator int(HashValue hashValue);

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

    public override bool Equals(object obj);
    public bool Equals(HashValue other);
    public override int GetHashCode();
}

E se nomearmos os tipos Hash e HashValue, e não os tipos de aninhamento?

Hash parece um nome muito geral para mim. Acho que precisamos ter HashCode no nome da API do ponto de entrada porque sua finalidade é ajudar a implementar GetHashCode() , não GetHash() .

alguém pode querer coletar uma lista de hashes e comunicar isso ao leitor uma listaé usado em vez de uma lista. Isso pode não funcionar tão bem se fizermos o tipo aninhado: List(mesmo listaé meio confuso à primeira vista).

Este parece ser um caso de uso improvável - não temos certeza se devemos otimizar o design para ele.

as únicas APIs BCL que consigo pensar em que temos tipos aninhados

TimeZoneInfo.AdjustmentRule e TimeZoneInfo.TransitionTime são exemplos na BCL que foram intencionalmente adicionados como tipos aninhados.

@justinvp

Acho que precisamos ter HashCode no nome da API de ponto de entrada porque sua finalidade é ajudar a implementar GetHashCode (), não GetHash ().

👍 Entendo.

Eu pensei sobre as coisas um pouco mais. Parece razoável ter uma estrutura aninhada; como você mencionou, a maioria das pessoas nunca verá o tipo real. Só uma coisa: acho que o tipo deveria ser Seed , em vez de HashCodeValue . O contexto de seu nome já está implícito na classe que o contém.

API proposta

namespace System
{
    public static class HashCode
    {
        public static Seed Combine(int hash);
        public static Seed Combine<T>(T obj);
        public static Seed Combine<T>(T obj, IEqualityComparer<T> comparer);

        public struct Seed : IEquatable<Seed>
        {
            public Seed Combine(int hash);
            public Seed Combine<T>(T obj);
            public Seed Combine<T>(T obj, IEqualityComparer<T> comparer);

            public int Value { get; }

            public static implicit operator int(Seed seed);

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

            public bool Equals(Seed other);
            public override bool Equals(object obj);
            public override int GetHashCode();
        }
    }
}

@jamesqo Qualquer objeção ou problema de implementação em ter public readonly int Value vez disso? O problema de Seed é que não é tecnicamente uma semente após a primeira combinação.

Também concordo com @justinvp , Hash deve ser reservado para lidar com hashes. Isso foi introduzido para simplificar o trato com HashCode .

@redknightlois Para ser claro, estávamos falando sobre o nome da estrutura, não o nome da propriedade.

        public struct Seed : IEquatable<Seed>
        {
            public Seed Combine(int hash);
            public Seed Combine<T>(T obj);
            public Seed Combine<T>(T obj, IEqualityComparer<T> comparer);

            public int Value { get; }

            public static implicit operator int(Seed seed);

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

            public bool Equals(Seed other);
            public override bool Equals(object obj);
            public override int GetHashCode();
        }

Uso:
c# int hashCode = HashCode.Combine(field1).Combine(name, StringComparison.OrdinalIgnoreCase).Value; int hashCode = (int)HashCode.Combine(field1).Combine(field2);

O problema com a semente é que não é tecnicamente uma semente após a primeira colheita.

É uma semente para a próxima colheitadeira, que produz uma nova semente.

Alguma objeção ou problema de implementação com relação ao valor int público somente leitura?

Por quê? int Value { get; } é mais idiomático e pode ser facilmente embutido.

É uma semente para a próxima colheitadeira, que produz uma nova semente.

Não seria uma muda? ;)

@jamesqo Em minha experiência, quando cercado por propriedades de código complexas, tende a gerar códigos piores do que campos (entre aqueles, não embutidos). Além disso, um campo somente leitura de um único int em uma estrutura é traduzido diretamente em um registro e, eventualmente, quando o JIT usa somente leitura para otimização (que não conseguiu encontrar qualquer uso dele ainda em relação à geração de código); há otimizações que podem ser permitidas porque podem raciocinar que é somente leitura. Do ponto de vista do uso, não há realmente diferente de um único getter.

EDIT: Além disso, também empurra a ideia de que essas estruturas são realmente imutáveis.

Na minha experiência, quando cercado por propriedades de código complexas, tende a gerar códigos piores do que os campos (entre aqueles, não embutidos).

Se você encontrar uma única compilação não de depuração em que uma propriedade implementada automaticamente nem sempre está alinhada, isso é um problema de JIT e deve ser definitivamente corrigido.

Além disso, um campo somente leitura de um único int em uma estrutura se traduz diretamente em um registrador

há otimizações que podem ser permitidas porque podem raciocinar que é somente leitura.

O campo de apoio desta estrutura será somente leitura; a API será um acessador.

Não acho que usar uma propriedade afetará o desempenho de forma alguma aqui.

@jamesqo Vou ter isso em mente quando os encontrar. Para código sensível ao desempenho, eu simplesmente não uso mais propriedades por causa disso (memória muscular neste ponto).

Você pode considerar chamar a estrutura aninhada de "Estado" em vez de "Semente"?

@ellismg Claro, obrigado pela sugestão. Eu estava lutando para encontrar um bom nome para a estrutura interna.

@karelz Acho que esta API finalmente está

@jamesqo @JonHanna por que precisamos de Combine<T>(T obj) vez de Combine(object o) ?

por que precisamos do Combine(T obj) em vez de Combine (objeto o)?

O último seria alocado se a instância fosse uma estrutura.

duh, obrigado pelo esclarecimento.

Não gostamos do tipo aninhado porque parece complicar o design. A raiz do problema é que não podemos nomear o estático e o não-estático da mesma forma. Temos duas opções: livrar-se da estática ou renomear. Achamos que renomear para Create faz mais sentido, pois cria um código razoavelmente legível, em comparação com o uso do construtor padrão.

A menos que haja alguma oposição forte, esse é o design que escolhemos:

`` `C #
sistema de namespace
{
public struct HashCode: IEquatable
{
Criar HashCode estático público (int hashCode);
public static HashCode Create(T obj);
public static HashCode Create(T obj, IEqualityComparercomparador);

    public HashCode Combine(int hashCode);
    public HashCode Combine<T>(T obj);
    public HashCode Combine<T>(T obj, IEqualityComparer<T> comparer);

    public int Value { get; }

    public static implicit operator int(HashCode hashCode);

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

    public bool Equals(HashCode other);
    public override bool Equals(object obj);
    public override int GetHashCode();
}

}
`` `

Vamos esperar alguns dias por feedback adicional para descobrir se há um feedback forte sobre a proposta aprovada. Então, podemos torná-lo 'disponível'.

Por que isso complica o design? Eu poderia entender como seria ruim se realmente tivéssemos que usar o HashCode.State no código (por exemplo, para definir o tipo de uma variável), mas esperamos que seja esse o caso? Na maioria das vezes, acabarei retornando o valor imediatamente ou convertendo para um int e armazenando-o.

Acho que a combinação de Criar e Combinar é pior.

Consulte https://github.com/dotnet/corefx/issues/8034#issuecomment -262661653

@terrajobst

Achamos que renomear para Create faz mais sentido, pois cria um código razoavelmente legível, em comparação com o uso do construtor padrão.

A menos que haja alguma oposição forte, esse é o design que escolhemos:

Eu ouvi você, mas tive um pensamento de última hora enquanto estava trabalhando na implementação ... poderíamos simplesmente adicionar uma propriedade estática Zero / Empty a HashCode , e as pessoas ligam para Combine daí? Isso nos livraria de ter que ter métodos Combine / Create separados.

namespace System
{
    public struct HashCode : IEquatable<HashCode>
    {
        public static HashCode Empty { get; }

        public HashCode Combine(int hashCode);
        public HashCode Combine<T>(T obj);
        public HashCode Combine<T>(T obj, IEqualityComparer<T> comparer);

        public int Value { get; }

        public static implicit operator int(HashCode hashCode);

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

        public bool Equals(HashCode other);
        public override bool Equals(object obj);
        public override int GetHashCode();
    }
}

int GetHashCode()
{
    return HashCode.Empty
        .Combine(_1)
        .Combine(_2);
}

Alguém mais acha que isso é uma boa ideia? (Vou enviar um PR enquanto isso e, se as pessoas acharem que sim, vou alterá-lo no PR.)

@jamesqo , gosto da ideia Vazia / Zero.

Eu ficaria bem com isso (nenhuma preferência forte entre Empty vs. Create factory) ... @weshaggard @bartonjs @stephentoub @terrajobst o que vocês acham?

Pessoalmente, acho que Create () é melhor; mas eu gosto mais de HashCode.Empty que new HashCode() .

Uma vez que permite uma versão que não tem operador novo, e não impede decidir depois que realmente queremos o Create como bootstrapper ... :: shrug ::.

Essa é a extensão total da minha resistência (também conhecida como não muito).

FWIW eu votaria em Create vez de Empty / Zero . Prefiro começar com um valor real do que suspender tudo em Empty / Zero . Parece / parece estranho.

Também desestimula as pessoas semeando com zero, que tende a ser uma semente ruim.

Eu prefiro Criar em vez de Vazio. Combina com a forma como penso a respeito: quero criar um código hash e adicionar valores adicionais. Eu ficaria bem com a abordagem aninhada também.

Embora eu fosse dizer que chamá-lo de Vazio não era uma boa ideia (e isso já foi dito), depois de um terceiro pensamento, ainda acho que não é uma solução ruim. Que tal algo como Builder. Embora ainda seja possível usar zero, a palavra meio que desencoraja você a usá-la imediatamente.

@JonHanna só para esclarecer: Você quis dizer isso como voto em Create , certo?

E em um quarto pensamento, que tal Com em vez de Criar.

HashCode.With (a) .Combine (b). Combine (c)

Exemplo de uso com base na discussão mais recente (com Create possivelmente substituído por um nome alternativo):

`` `c #
substituição pública int GetHashCode () =>
HashCode.Create (_field1) .Combine (_field2) .Combine (_field3);

We went down the path of this chaining approach, but didn't reconsider earlier proposals when the static & instance `Combine` methods didn't pan out...

Are we sure we don't want something like the existing `Path.Combine` pattern, that was proposed previously, with a handful of generic `Combine` overloads? e.g.:

```c#
public override int GetHashCode() =>
    HashCode.Combine(_field1, _field2, _field3);

@justinvp levaria a código inconsistente + mais jitting, acho b / c de combinações mais genéricas. Sempre podemos revisitar isso em outra edição, se for desejável.

Pelo que vale a pena, eu prefiro a versão proposta originalmente, pelo menos no uso (não tenho certeza sobre os comentários sobre o tamanho do código, jitting, etc.). Parece um exagero ter uma estrutura extra e mais de 10 membros diferentes para algo que poderia ser expresso como um método com algumas sobrecargas de aridade diferente. Eu também não sou um fã de APIs de estilo fluente em geral, então talvez isso esteja influenciando minha opinião.

Eu não ia mencionar isso porque é um pouco incomum e ainda não tenho certeza de como me sinto a respeito, mas aqui está outra ideia, apenas para ter certeza de que todas as alternativas foram consideradas ...

E se fizéssemos algo semelhante ao mutável HashCodeCombiner "builder" do ASP.NET Core, com métodos Add semelhantes, mas também incluíssemos suporte para a sintaxe do inicializador de coleção?

Uso:

`` `c #
substituição pública int GetHashCode () =>
novo HashCode {_field1, _field2, _field3};

With a surface area something like:

```c#
namespace System
{
    public struct HashCode : IEquatable<HashCode>, IEnumerable
    {
        public void Add(int hashCode);
        public void Add<T>(T obj);
        public void Add<T>(T obj, IEqualityComparer<T> comparer);

        public int Value { get; }

        public static implicit operator int(HashCode hashCode);

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

        public bool Equals(HashCode other);
        public override bool Equals(object obj);
        public override int GetHashCode();

        IEnumerator IEnumerable.GetEnumerator();
    }
}

Ele teria que implementar IEnumerable no mínimo junto com pelo menos um método Add para habilitar a sintaxe do inicializador de coleção. IEnumerable poderia ser implementado explicitamente para ocultá-lo do intellisense e GetEnumerator poderia lançar NotSupportedException ou retornar o valor do código hash como um único item combinado no enumerável, se acontecer de alguém usá-lo (o que seria raro).

@justinvp , você tem uma ideia interessante. No entanto, discordo respeitosamente; Acho que HashCode deve ser mantido como imutável para evitar pegadinhas com estruturas mutáveis. Também ter que implementar IEnumerable para isso parece meio artificial / fragmentado; se alguém tiver uma diretiva using System.Linq no arquivo, Cast<> e OfType<> aparecerão como métodos de extensão se colocarem um ponto próximo a HashCode . Acho que devemos ficar mais perto da proposta atual.

@jamesqo , concordo - daí a minha hesitação em sequer mencioná-lo. A única coisa que gosto nisso é que o uso pode ser mais limpo do que o encadeamento, mas isso por si só é outra desvantagem, pois não está claro se os inicializadores de coleção podem ser usados ​​sem ver o uso de amostra.

@MadsTorgersen , @jaredpar , por que o inicializador de coleção requer a implementação de IEnumerable \Terceiro comentário de @justinvp acima.

@jamesqo , concordo que é melhor manter isso imutável (e não IEnumerable \

@mellinoe Acho que isso tornaria o caso simples um pouco mais simples, mas também tornaria qualquer coisa além disso mais complicada (e menos claro sobre o que é a coisa certa a fazer).

Isso inclui:

  1. mais itens do que você tem sobrecargas para
  2. condições
  3. rotações
  4. usando comparador

Considere o código do ASP.NET postado antes neste tópico (atualizado para a proposta atual):

`` `c #
var hashCode = HashCode
.Create (IsMainPage)
.Combine (ViewName, StringComparer.Ordinal)
.Combine (ControllerName, StringComparer.Ordinal)
.Combine (AreaName, StringComparer.Ordinal);

if (ViewLocationExpanderValues! = null)
{
foreach (item var em ViewLocationExpanderValues)
{
hashCode = hashCode
.Combine (item.Key, StringComparer.Ordinal)
.Combine (item.Value, StringComparer.Ordinal);
}
}

return hashCode;

How would this look with the original `Hash.CombineHashCodes`? I think it would be:

```c#
var hashCode = Hash.CombineHashCodes(
    IsMainPage,
    StringComparer.Ordinal.GetHashCode(ViewName),
    StringComparer.Ordinal.GetHashCode(ControllerName),
    StringComparer.Ordinal.GetHashCode(AreaName));

if (ViewLocationExpanderValues != null)
{
    foreach (var item in ViewLocationExpanderValues)
    {
        hashCode = Hash.CombineHashCodes(
            hashCode
            StringComparer.Ordinal.GetHashCode(item.Key),
            StringComparer.Ordinal.GetHashCode(item.Value));
    }
}

return hashCode;

Mesmo se você ignorar a chamada de GetHashCode() para comparadores personalizados, acho que ter que passar o valor anterior de hashCode como o primeiro parâmetro não é simples.

@KrzysztofCwalina De acordo com a nota de @ericlippert em The C # Programming Language 1 , é porque os inicializadores de coleção são (sem surpresa) destinados a serem sintetizadores para a criação de coleções, não para a aritmética (que era o outro uso comum do método denominado Add ).

1 Devido à forma como o Google Livros funciona, esse link pode não funcionar para todos.

@KrzysztofCwalina , e observe, ele requer IEnumerable não genérico, não IEnumerable<T> .

@svick , menor nit em seu primeiro exemplo acima: a primeira chamada para .Combine seria .Create com a proposta atual. A menos que usemos a abordagem aninhada.

@svick

também tornaria tudo além disso mais complicado (e menos claro sobre o que é a coisa certa a fazer)

Eu não sei, o segundo exemplo é pouco diferente do primeiro no geral, e não é mais complexo da OMI. Com a segunda abordagem original, você apenas passa um monte de códigos hash (acho que o primeiro parâmetro deveria ser IsMainPage.GetHashCode() ), então me parece direto. Mas parece que estou em minoria aqui, então não vou insistir na abordagem original. Não tenho uma opinião forte; ambos os exemplos parecem razoáveis ​​o suficiente para mim.

@justinvp Obrigado, atualizado. (Fui com a primeira proposta no primeiro post, e não percebi que ela estava desatualizada, alguém provavelmente deveria atualizá-la.)

@mellinoe o problema é que o segundo pode gerar bugs sutis. Este é o código real de um de nossos projetos.

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public int GetHashCode(PageFromScratchBuffer obj)
        {
            int v = Hashing.Combine(obj.NumberOfPages, obj.ScratchFileNumber);
            int w = Hashing.Combine(obj.Size.GetHashCode(), obj.PositionInScratchBuffer.GetHashCode());
            return Hashing.Combine(v, w);            
        }

Vivemos com isso, mas lidamos com coisas de nível muito baixo todos os dias; portanto, não o desenvolvedor médio, com certeza. No entanto, não é o mesmo aqui combinar v com w do que w com v ... o mesmo entre v e w combina. As combinações de hash não são comutativas, portanto, o encadeamento uma após a outra pode, na verdade, se livrar de todo um conjunto de erros no nível da API.

Fui com a primeira proposta no primeiro post, e não percebi que está desatualizada, provavelmente alguém deveria atualizá-la.

Feito.
BTW: Esta proposta é muito difícil de acompanhar, especialmente os votos ... tantas variações (o que eu acho que é bom ;-))

@karelz Se adicionarmos Create APIs, então acho que ainda podemos adicionar Empty . Não precisa ser um ou outro, como disse @bartonjs . Proposto

namespace System
{
    public struct HashCode : IEquatable<HashCode>
    {
        public HashCode();

        public static HashCode Empty { get; }

        public static HashCode Create(int hashCode);
        public static HashCode Create<T>(T value);
        public static HashCode Create<T>(T value, IEqualityComparer<T> comparer);

        public HashCode Combine(int hashCode);
        public HashCode Combine<T>(T value);
        public HashCode Combine<T>(T value, IEqualityComparer<T> comparer);

        public int Value { get; }

        public static implicit operator int(HashCode hashCode);

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

        public bool Equals(HashCode other);
        public override bool Equals(object obj);
        public override int GetHashCode();
        public override string ToString();
    }
}

@JonHanna

Também desestimula as pessoas semeando com zero, que tende a ser uma semente ruim.

O algoritmo de hash que estamos escolhendo será o mesmo usado em HashHelpers hoje, que tem o efeito de hash(0, x) == x . HashCode.Empty.Combine(x) produzirá exatamente os mesmos resultados que HashCode.Create(x) , portanto, objetivamente, não há diferença.

@jamesqo você se esqueceu de incluir o Zero em sua última proposta. Se isso foi uma omissão, você pode atualizá-lo? Podemos então pedir às pessoas que votem em sua proposta mais recente. Parece que as outras alternativas (veja o post principal que atualizei) não obtêm muito seguimento ...

@karelz Obrigado por detectar, corrigido.

@KrzysztofCwalina para verificar se você quer dizer “Adicionar” no sentido de adicionar a uma coleção, não em algum outro sentido. Não sei se gosto dessa restrição, mas foi o que decidimos na época.

public static HashCode Create(int hash);
public HashCode Combine(int hash);

O parâmetro deve ser denominado hashCode vez de hash uma vez que o valor passado será um código hash provavelmente obtido da chamada de GetHashCode() ?

Empty / Zero

Se acabarmos mantendo isso, outro nome a considerar é Default .

@justinvp

O parâmetro deve ser denominado hashCode em vez de hash, uma vez que o valor passado será um código hash provavelmente obtido ao chamar GetHashCode ()?

Eu queria nomear os parâmetros int hash e os HashCode parâmetros hashCode . Pensando bem, entretanto, acredito que hashCode seria melhor porque, como você mencionou, hash é meio vago. Vou atualizar a API.

Se acabarmos mantendo isso, outro nome a considerar é Padrão.

Quando ouço Default penso "a maneira normal de fazer algo quando você não sabe qual opção escolher", não "o valor padrão de uma estrutura". por exemplo, algo como Encoding.Default tem uma conotação completamente diferente.

O algoritmo de hash que estamos escolhendo será o mesmo usado em HashHelpers hoje, que tem o efeito de hash (0, x) == x. HashCode.Empty.Combine (x) produzirá exatamente os mesmos resultados que HashCode.Create (x), portanto, objetivamente, não há diferença.

Como alguém que não sabe muito sobre isso, eu realmente gosto da simplicidade de HashCode.Create(x).Combine(...) . Create é muito óbvio, porque é usado em muitos outros lugares.

Se Empty / Zero / Default não fornece nenhum uso algorítmico, ele não deveria estar lá IMO.

PS: tópico muito interessante !! Bom trabalho! 👍

@ cwe1ss

Se Vazio / Zero / Padrão não fornecer nenhum uso algorítmico, não deveria estar lá IMO.

Ter um campo Empty fornece uso algorítmico. Ele representa um "valor inicial" a partir do qual você pode combinar hashes. Por exemplo, se você deseja combinar uma matriz de hashes estritamente usando Create , é muito doloroso:

int CombineRange(int[] hashes)
{
    if (hashes.Length == 0)
    {
        return 0;
    }

    var result = HashCode.Create(hashes[0]);

    for (int i = 1; i < hashes.Length; i++)
    {
        result = result.Combine(hashes[i]);
    }

    return result;
}

Se você tem Empty , torna-se muito mais natural:

int CombineRange(int[] hashes)
{
    var result = HashCode.Empty;

    for (int i = 0; i < hashes.Length; i++)
    {
        result = result.Combine(hashes[i]);
    }

    return result;
}

// or

int CombineRange(int[] hashes)
{
    return hashes.Aggregate(HashCode.Empty, (hc, next) => hc.Combine(next));
}

@terrajobst Este tipo é bastante análogo a ImmutableArray<T> para mim. Um array vazio não é muito útil por si só, mas é muito útil como um "ponto de partida" para outras operações, e é por isso que temos uma propriedade Empty para ele. Acho que faria sentido ter um por HashCode também; estamos mantendo Create .

@jamesqo Percebi que você silenciosamente / por acidente mudou o nome do arg obj para value em sua proposta https://github.com/dotnet/corefx/issues/8034#issuecomment -262661653. Mudei de volta para obj que IMO captura melhor o que você obtém. O nome value está mais associado ao próprio valor de hash "int" neste contexto.
Estou aberto a novas discussões sobre o nome do argumento, se necessário, mas vamos alterá-lo propositalmente e acompanhar a diferença em relação à última proposta aprovada.

Eu atualizei a proposta no topo. Eu também chamei diff contra a última versão aprovada da proposta.

O algoritmo de hash que escolhemos será o mesmo usado nos HashHelpers hoje

Por que é um bom algoritmo a ser escolhido como aquele que deve ser usado em todos os lugares? Que suposição ele fará sobre os hashcodes sendo combinados? Se for usado em todos os lugares, abrirá novos caminhos para ataques DDoS? (Observe que isso nos prejudicou devido ao hash de string no passado.)

E se fizéssemos algo semelhante ao "construtor" mutável do HashCodeCombiner do ASP.NET Core

Acho que esse é o padrão certo a ser usado. Um bom combinador de hashcode universal geralmente pode usar mais estado do que o que cabe no próprio hashcode, mas então o padrão fluente quebra porque passar structs maiores é um problema de desempenho.

Por que é um bom algoritmo a ser escolhido como aquele que deve ser usado em todos os lugares?

Não deve ser usado em todos os lugares. Veja meu comentário em https://github.com/dotnet/corefx/issues/8034#issuecomment -260790829; ele é voltado principalmente para pessoas que não sabem muito sobre hashing. Pessoas que sabem o que estão fazendo podem avaliá-lo para ver se ele atende às suas necessidades.

Que suposição ele fará sobre os hashcodes sendo combinados? Se for usado em todos os lugares, abrirá novos caminhos para ataques DDoS?

Um problema com o hash atual que temos é hash(0, x) == x . Portanto, se uma série de nulos ou zeros for alimentada para o hash, ele permanecerá como 0. Consulte o código . Isso não quer dizer que nulos não contam, mas nenhum dos nulos iniciais sim. Estou pensando em usar algo mais robusto (mas um pouco mais caro) como aqui , que adiciona uma constante mágica para evitar o mapeamento de zero a zero.

Acho que esse é o padrão certo a ser usado. Um bom combinador de hashcode universal geralmente pode usar mais estado do que o que cabe no próprio hashcode, mas então o padrão fluente quebra porque passar structs maiores é um problema de desempenho.

Não acho que deveria haver um combinador universal com um tamanho de estrutura grande que tente se adequar a todos os casos de uso. Em vez disso, eu estava imaginando tipos de código hash separados que são todos de tamanho interno ( FnvHashCode , etc.) e todos têm seus próprios métodos Combine . Além disso, esses tipos de "construtor" serão mantidos no mesmo método de qualquer maneira, não transmitidos.

Não acho que deveria haver um combinador universal com um tamanho de estrutura grande que tente se adequar a todos os casos de uso.

O ASP.NET Core será capaz de substituir seu próprio combinador de hashcode - que tem 64 bits de estado atualmente - por este?

Eu estava imaginando tipos de código hash separados, todos de tamanho interno (FnvHashCode, etc.)

Isso não leva a uma explosão combinatória? Deve fazer parte da proposta da API para deixar claro aonde esse design da API leva.

Concordo que é uma boa ideia ter um combinador de hashcode que seja óbvio para usar em 99% dos casos. No entanto, ele precisa permitir mais estado interno do que apenas 32 bits.

BTW: ASP.NET usava o padrão fluent para combinação de hashcode originalmente, mas parou de fazer isso porque levava a erros fáceis de perder: https://github.com/aspnet/Razor/pull/537

@jkotas em relação à segurança de inundação de hash.
AVISO LEGAL: Não é um especialista (você deve consultar um e o MS tem mais do que alguns sobre o assunto) .

Tenho olhado em volta e, embora não seja o consenso geral sobre o assunto, há um argumento que está ganhando força hoje em dia. Os códigos hash têm tamanho de 32 bits, que postei antes de um gráfico que mostra a probabilidade de colisões dado o tamanho do conjunto. Isso significa que não importa o quão bom seja o seu algoritmo (olhando para SipHash, por exemplo), é bastante viável gerar muitos hashes e encontrar colisões em um tempo razoável (falando em menos de uma hora). Esses problemas precisam ser resolvidos na estrutura de dados que contém os hashes, eles não podem ser resolvidos no nível da função de hash. Pagar desempenho extra em não criptográficos para proteger contra inundação de hash sem corrigir a estrutura de dados subjacente não resolverá o problema.

EDIT: Você postou enquanto eu estava escrevendo. À luz disso, quais são os ganhos do estado de 64 bits para você?

@jkotas Eu investiguei o problema ao qual você se vinculou. Diz:

Reação ao aspnet / Common # 40

Descrição de https://github.com/aspnet/Common/issues/40 :

Identifique o bug:

public class TagBuilder
{
    private Dictionary<string, string> _attributes;
    private string _tagName;
    private string _innerContent;

    public override int GetHashCode()
    {
        var hash = HashCodeCombiner.Start()
            .Add(_tagName, StringComparer.Ordinal)
            .Add(_innerContent, StringComparer.Ordinal);

        foreach (var kvp in _attributes)
        {
            hash.Add(kvp.Key, StringComparer.Ordinal).Add(kvp.Value, StringComparer.Ordinal);
        }

        return hash.Build();
    }
}

Vamos. Esse argumento é como dizer que string deve ser mutável, pois as pessoas não percebem que Substring retorna uma nova string. Structs mutáveis ​​são muito piores em termos de pegadinhas; Acho que devemos manter a estrutura imutável.

em relação à segurança de inundação de hash.

Há dois lados disso: projeto correto por construção (estruturas de dados robustas, etc.); e mitigação dos problemas no design existente. Ambos são importantes.

@karelz Com relação à nomenclatura de parâmetros

Percebi que você silenciosamente / por acidente mudou arg name obj para value em sua proposta dotnet / corefx # 8034 (comentário). Mudei de volta para obj que IMO captura melhor o que você obtém. O valor do nome está mais associado ao próprio valor de hash "int" neste contexto.
Estou aberto a novas discussões sobre o nome do argumento, se necessário, mas vamos alterá-lo propositalmente e acompanhar a diferença em relação à última proposta aprovada.

Estou considerando, em uma proposta futura, adicionar APIs para combinar valores em massa. Por exemplo: CombineRange(ReadOnlySpan<T>) . Se nomearmos obj , teríamos que nomear o parâmetro objs , o que soa muito estranho. Portanto, devemos chamá-lo de item ; no futuro, podemos nomear o parâmetro span items . Atualizou a proposta.

@jkotas concorda, mas o ponto aqui é que não estamos mitigando nada no nível do combinador ...

A única coisa que podemos fazer é ter uma semente aleatória, que para todos os estados e propósitos eu me lembro de ter visto o código em string e é corrigido por build. (pode estar errado sobre isso, porque isso foi há muito tempo, no entanto). Ter uma implementação adequada de sementes aleatórias é a única mitigação que poderia ser aplicada aqui.

Este é um desafio, dê-me sua melhor string e / ou função hash de memória com uma semente aleatória fixa e eu irei construir um conjunto de códigos hash de 32 bits que gerará apenas colisões. Não tenho medo de lançar tal desafio porque é muito fácil de fazer, a teoria da probabilidade está do meu lado. Eu até iria fazer uma aposta, mas sei que vou ganhar, então, essencialmente, não é mais uma aposta.

Além disso ... uma análise mais profunda mostra que, mesmo que a mitigação seja a capacidade de ter essas "sementes aleatórias" embutidas por corrida, um combinador mais complicado não é necessário. Porque essencialmente você atenuou o problema na fonte.

Digamos que você tenha M1 e M2 com diferentes sementes aleatórias rs1 e rs2 ....
M1 emitirá h1 = hash('a', rs1) e h2=hash('b', rs1)
M2 emitirá h1' = hash('a', rs2) e h2'=hash('b', rs2)
O ponto chave aqui é que h1 e h1' irão diferir com uma probabilidade de 1/ (int.MaxInt-1) (se hash é bom o suficiente) que para todos os efeitos é tão bom quanto vai ficar.
Portanto, qualquer c(x,y) você decida usar (se for bom o suficiente) já está levando em consideração a mitigação embutida na fonte.

EDIT: Achei o código, você está usando Marvin32 que muda em cada domínio agora. Portanto, a mitigação para strings é o uso de sementes aleatórias por execução. O que, como afirmei, é uma mitigação boa o suficiente.

@jkotas

O ASP.NET Core será capaz de substituir seu próprio combinador de hashcode - que tem 64 bits de estado atualmente - por este?

Absolutamente; ele usa o mesmo algoritmo de hash. Acabei de criar este aplicativo de teste para medir o número de colisões e executá-lo 10 vezes. Nenhuma diferença significativa com o uso de 64 bits.

Eu estava imaginando tipos de código hash separados, todos de tamanho interno (FnvHashCode, etc.)

Isso não leva a uma explosão combinatória? Deve fazer parte da proposta da API para deixar claro aonde esse design da API leva.

@jkotas , não vai. O design desta classe não definirá o design para futuras APIs de hash em pedra. Esses devem ser considerados cenários mais avançados, devem ir em uma proposta diferente, como dotnet / corefx # 13757, e terão uma discussão de design diferente. Acredito que seja muito mais importante ter uma API simples para um algoritmo de hash geral, para iniciantes que estão lutando para substituir GetHashCode .

Concordo que é uma boa ideia ter um combinador de hashcode que seja óbvio para usar em 99% dos casos. No entanto, ele precisa permitir mais estado interno do que apenas 32 bits.

Quando precisaríamos de mais estado interno do que 32 bits? editar: Se for para permitir que as pessoas conectem a lógica de hashing personalizada, acho (de novo) que deve ser considerado um cenário avançado e ser discutido em dotnet / corefx # 13757.

você está usando o Marvin32 que muda em cada domínio agora

Certo, a atenuação da aleatorização do código de hash da string é habilitada por padrão no .NET Core. Não é habilitado por padrão para aplicativos independentes em .NET Framework completo por causa da compatibilidade; ele só é ativado por meio de peculiaridades (por exemplo, em ambientes de alto risco).

Ainda temos o código para hash não aleatório no .NET Core, mas não há problema em excluí-lo. Não espero que precisemos dele novamente. Isso também tornaria o cálculo do código hash da string um pouco mais rápido, porque não haverá mais a verificação de se usar o caminho não aleatório.

O algoritmo Marvin32 usado para calcular os hashcodes da string aleatória tem estado interno de 64 bits. Ele foi escolhido pelos especialistas no assunto MS. Tenho certeza de que eles tinham um bom motivo para usar o estado interno de 64 bits e não o usaram apenas para tornar as coisas mais lentas.

Um combinador de hash de propósito geral deve continuar evoluindo essa mitigação: ele deve usar uma semente aleatória e um algoritmo de combinação de hashcode forte o suficiente. Idealmente, ele usaria o mesmo Marvin32 como hashing de string aleatório.

O algoritmo Marvin32 usado para calcular os hashcodes da string aleatória tem estado interno de 64 bits. Ele foi escolhido pelos especialistas no assunto MS. Tenho certeza de que eles tinham um bom motivo para usar o estado interno de 64 bits e não o usaram apenas para tornar as coisas mais lentas.

@jkotas , o combinador de código hash ao qual você vinculou não usa Marvin32. Ele usa o mesmo algoritmo DJBx33x ingênuo usado por string.GetHashCode não aleatório.

Um combinador de hash de propósito geral deve continuar evoluindo essa mitigação: ele deve usar uma semente aleatória e um algoritmo de combinação de hashcode forte o suficiente. Idealmente, ele usaria o mesmo Marvin32 como hashing de string aleatório.

Esse tipo não deve ser usado em locais propensos a ataques de hash DoS. Ele é direcionado para pessoas que não sabem como adicionar / xor e ajudará a prevenir coisas como https://github.com/dotnet/coreclr/pull/4654.

Um combinador de hash de propósito geral deve continuar evoluindo essa mitigação: ele deve usar uma semente aleatória e um algoritmo de combinação de hashcode forte o suficiente. Idealmente, ele usaria o mesmo Marvin32 como hashing de string aleatório.

Então, devemos conversar com a equipe C # para que implementem um algoritmo de hash ValueTuple atenuado. Porque esse código também será usado em ambientes de alto risco. E, claro, Tuple https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Tuple.cs#L60 ou System.Numerics.HashHelpers (usado em todo o Lugar, colocar).

Agora, antes de decidirmos como implementá-lo, eu examinaria os mesmos especialistas no assunto se pagar o custo de um algoritmo de combinação de hashcode totalmente aleatório vale a pena (se existir, é claro), mesmo que não mude a forma como a API é projetado também (sob a API proposta, você pode usar um estado de 512 bits e ainda ter a mesma API pública, se você estiver disposto a pagar o custo disso, é claro).

Este é direcionado para pessoas que não sabem melhor adicionar / xor

É exatamente por isso que é importante que seja robusto. O valor principal do .NET é que ele aborda problemas para pessoas que não conhecem melhor.

E já que estamos nisso, não vamos nos esquecer de IntPtr https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/IntPtr.cs#L119
Aquele é especialmente desagradável, xor é provavelmente o pior que existe porque bad colidirá com dab .

implementar um algoritmo de hash ValueTuple mitigado

Bom ponto. Não tenho certeza se o ValueTuple foi enviado ou se ainda é hora de fazer isso. Https://github.com/dotnet/corefx/issues/14046 aberto

não vamos nos esquecer do IntPtr

Esses são erros do passado ... a barreira para consertá-los é muito maior.

@jkotas

Esses são erros do passado ... a barreira para consertá-los é muito maior.

Achei que um dos pontos do .Net Core é que a barreira para "pequenas" mudanças como essa deve ser muito menor. Se alguém depende da implementação de IntPtr.GetHashCode (o que realmente não deveria), ele pode escolher não atualizar sua versão do .Net Core.

a barra para "pequenas" mudanças como essa deve ser muito menor

Sim, é - em comparação com o .NET Framework completo. Mas você ainda tem que fazer o trabalho para que a mudança seja empurrada pelo sistema e pode descobrir que simplesmente não vale a pena o sofrimento. Um exemplo recente é a mudança no algoritmo de hashing Tuple<T> que foi revertido por causa da quebra do F #: https://github.com/dotnet/coreclr/pull/6767#issuecomment -256896016

@jkotas

Se fôssemos fazer HashCode 64 bits, você acha que um design imutável mataria o perf em ambientes de 32 bits? Concordo com outros leitores, um padrão de construtor parece ser muito pior.

Mate o desempenho - não. Penalidade de desempenho paga por sintaxe sugar - sim.

Penalidade de desempenho paga por sintaxe sugar - sim.

É algo que poderia ser otimizado pelo JIT no futuro?

Mata o desempenho - não.
Penalidade de desempenho paga por sintaxe sugar - sim.

É mais do que açúcar sintático. Se estivéssemos dispostos a fazer de HashCode uma classe, então seria um açúcar sintático. Mas um tipo de valor mutável é um bug farm.

Citando você anteriormente:

É exatamente por isso que é importante que seja robusto. O valor principal do .NET é que ele aborda problemas para pessoas que não conhecem melhor.

Eu diria que um tipo de valor mutável não é uma API robusta para a maioria das pessoas que não sabem disso.

Eu diria que um tipo de valor mutável não é uma API robusta para a maioria das pessoas que não sabem disso.

Aceita. Pensei que é uma pena que seja o caso de tipos de construtor de estrutura mutáveis. Eu uso -os todos a tempo por causa do que eles são bons e apertado. [MustNotCopy] anotações alguém?

MustNotCopy é o sonho de qualquer amante de estruturas que se tornou realidade. @jaredpar?

MustNotCopy é apenas como uma pilha, mas ainda mais difícil de usar 😄

Eu sugiro não criar nenhuma classe, mas sim métodos de extensão para combinar hash

static class HashHelpers
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int CombineHash(this int hash1, int hash2);
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int CombineHash<T>(this int hash, T value);
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int CombineHash<T>(this int hash, T value, IEqualityComparer<T> comparer);
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int CombineHash<T>(this int hash, IEnumerable<T> values);
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static int CombineHash<T>(this int hash, IEnumerable<T> values, IEqualityComparer<T> comparer);
}

Isso é tudo! É rápido e fácil de usar.

@AlexRadch Não gosto que isso polua a lista de métodos para todos os inteiros , não apenas aqueles significados como hashes.

Além disso, você tem métodos que continuam uma cadeia de computação do código hash, mas como iniciá-lo? Você tem que fazer algo não óbvio, como começar do zero? Ou seja, 0.CombineHash(this.FirstName).CombineHash(this.LastName) .

Atualização: de acordo com o comentário em dotnet / corefx # 14046, foi decidido que a fórmula de hash existente seria mantida por ValueTuple :

@jamesqo Obrigado pela ajuda.
Da última discussão com @jkotas e @VSadov , estamos ok para avançar com a randomização / semeadura, mas preferimos não adotar uma função hash mais cara.
Fazer a randomização impede a alteração da função hash no futuro, se necessário.

@jkotas , podemos apenas manter o hash baseado em ROL 5 atual para HashCode então, e reduzi-lo para 4 bytes? Isso eliminaria todos os problemas com a cópia da estrutura. Podemos fazer com que HashCode.Empty represente um valor de hash aleatório.

@svick
Sim, isso polui os métodos para todos os inteiros, mas pode ser colocado em um espaço de nome separado e se você não trabalhar com hashes, você não o incluirá e não verá.

0.CombineHash(this.FirstName).CombineHash(this.LastName) deve ser escrito como this.FirstName.GetHash().CombineHash(this.LastName)

Para implementar a partir da semente, ele pode ter o próximo método estático

static class HashHelpers
{
    public static int ClassSeed<T>();
}

class SomeClass
{
    int GetHash()
    {
        return HashHelpers.ClassSeed<SomeClass>().CombineHash(value1).CombineHash(value2);
    }
}

Portanto, cada classe terá uma semente diferente para aleatorizar os hashes.

@jkotas , podemos apenas manter o hash baseado em ROL 5 para HashCode e reduzi-lo para 4 bytes?

Eu acho que um auxiliar de construção de código de hash de plataforma pública precisa usar o estado de 64 bits para ser robusto. Se for apenas de 32 bits, estará propenso a produzir resultados ruins quando for usado para fazer hash de mais elementos, matrizes ou coleções em particular. Como você escreve a documentação sobre quando é uma boa ideia usá-la ou não? Sim, são instruções extras gastas na mistura dos bits, mas não acho que isso importe. Este tipo de instruções é executado super rápido. Minha experiência é que é melhor fazer mais mixagem de bits do que menos, porque os efeitos de mixar muito pouco são muito mais severos do que fazer muito.

Além disso, ainda tenho dúvidas sobre a forma proposta da API. Eu acredito que o problema deve ser pensado como construção de código hash, não combinação de código hash. Talvez seja prematuro adicionar isso como API de plataforma, e devemos esperar para ver se um padrão melhor surge para isso. Isso não impede que alguém publique um pacote nuget (fonte) com esta API, ou corefx usando-o como auxiliar interno.

@jkotas tendo um estado de

Veja o que torna uma boa função hash (que alguns claramente são como xor : http://softwareengineering.stackexchange.com/questions/49550/which-hashing-algorithm-is-best-for-uniqueness-and -speed e https://research.neustar.biz/2012/02/02/choosing-a-good-hash-function-part-3/

@jamesqo BTW, acabei de perceber que o combinador não funcionará no caso de: "Na verdade, estou combinando hashes (não hashes de tempo de execução) porque a semente muda a cada vez." ... construtor público com semente?

@jkotas

Eu acho que um auxiliar de construção de código de hash de plataforma pública precisa usar o estado de 64 bits para ser robusto. Se for apenas de 32 bits, estará propenso a produzir resultados ruins quando for usado para fazer hash de mais elementos, matrizes ou coleções em particular.

Isso importa quando ele vai acabar sendo condensado em um único int no final?

@jamesqo Não realmente, o tamanho do estado depende apenas da função, não da robustez. Na verdade, você pode piorar sua função de hash se a combinação não for projetada para funcionar dessa maneira e, na melhor das hipóteses, você está desperdiçando recursos porque não pode adquirir aleatoriedade por meio da coerção.

Corolário: se você estiver coerente, tenha certeza de que a função é estatisticamente excelente ou é quase certo que você a tornará pior.

Isso depende da existência de correlação entre os itens. Se não houver correlação, o estado de 32 bits e o rotl simples (ou mesmo xor) funcionam perfeitamente. Se houver correlação, depende.

Considere se alguém usou isso para construir um código hash de string a partir de caracteres individuais. Não que seja provável que alguém realmente faça isso para string, mas demonstra o problema:

for (int i = 0; i < str.Length; i++)
   hashCodeBuilder.Add(str[i]);

Isso daria resultados ruins para strings com estado de 32 bits e rotl simples, porque os caracteres em strings do mundo real tendem a ser correlacionados. Com que frequência os itens para os quais isso é usado vão ser correlacionados e quão ruins isso daria? É difícil dizer, embora as coisas na vida real tendam a se correlacionar de maneiras inesperadas.

Será ótimo adicionar o próximo método para a randomização de Hash de suporte da API.

namespace System
{
    public struct HashCode : IEquatable<HashCode>
    {
       // add this
       public static HashCode CreateRandomized(Type type);
       // or add this
       public static HashCode CreateRandomized<T>();
    }
}

@jkotas Eu não testei, então acredito que você fez. Mas isso definitivamente diz algo sobre a função que pretendemos usar. Simplesmente não é bom o suficiente , pelo menos, se você quiser trocar velocidade por confiabilidade (ninguém pode fazer coisas estúpidas com isso). Eu, pela primeira vez, sou a favor do design de que esta não é uma função hashing não criptografada, mas uma maneira rápida de combinar códigos hash não correlacionados (que são tão aleatórios quanto possível).

Se o que queremos é que ninguém faça coisas estúpidas com ele, usar um estado de 64 bits não é consertar nada, estamos apenas escondendo o problema. Ainda seria possível criar uma entrada que explorará essa correlação. O que nos aponta mais uma vez para o mesmo argumento que apresentei 18 dias atrás. Veja: https://github.com/dotnet/corefx/issues/8034#issuecomment -261301533

Eu, pela primeira vez, sou a favor do design de que esta não é uma função de hash não criptografada, mas uma maneira rápida de combinar códigos hash não correlacionados

A maneira mais rápida de combinar códigos hash não correlacionados é xor ...

É verdade, mas sabemos que da última vez não funcionou tão bem (IntPtr me vem à mente). A rotação e o XOR (atual) são igualmente rápidos, sem perda se alguém colocar algum tipo de coisa correlacionada.

Adicione a randomização do código hash com public static HashCode CreateRandomized(Type type); ou com public static HashCode CreateRandomized<T>(); métodos ou com ambos.

@jkotas Acho que encontrei um padrão melhor para isso. E se usássemos retornos ref do C # 7? Em vez de retornar HashCode cada vez, retornaríamos ref HashCode que cabe em um registro.

public struct HashCode
{
    private readonly long _value;

    public ref HashCode Combine(int hashCode)
    {
        CombineCore(ref _value, hashCode); // note: modifies the struct in-place
        return ref this;
    }
}

O uso permanece o mesmo de antes:

return HashCode.Combine(1)
    .Combine(2).Combine(3);

A única desvantagem é que estamos de volta a uma estrutura mutável. Mas não acho que haja uma maneira de não ter cópia e imutabilidade ao mesmo tempo.

( ref this ainda não funciona, mas vejo um PR em Roslyn para ativá-lo aqui. )


@AlexRadch Não acho sensato combinar mais o hash com o tipo, uma vez que obter o código hash do tipo é caro.

@jamesqo public static HashCode CreateRandomized<T>(); não obtém o código do tipo hash. Ele cria um HashCode aleatório para este tipo.

@jamesqo " ref this ainda não funciona". Mesmo depois que o problema de Roslyn for corrigido, ref this não estará disponível para o repositório corefx por um tempo (não tenho certeza de quanto tempo, @stephentoub provavelmente pode definir expectativas).

A discussão do design não está convergindo aqui. Além disso, os 200 comentários são muito difíceis de seguir.
Planejamos pegar @jkotas na próxima semana e liberar a proposta na revisão da API na próxima terça-feira. Em seguida, postaremos a proposta de volta aqui para comentários adicionais.

Por outro lado: sugiro encerrar este problema e criar um novo com a "proposta abençoada" quando a tivermos na próxima semana para diminuir a carga de seguir a longa discussão. Deixe-me saber se você acha que é uma má ideia.

@jcouv Aceito que não funcione ainda, portanto, desde que possamos seguir este design quando for lançado. (Eu também acho que pode ser possível contornar isso temporariamente usando Unsafe .)

@karelz OK: smile: abrirei uma nova. Eu concordo; meu navegador não suporta mais de 200 comentários tão bem.

@karelz encontrei um obstáculo; Acontece que o PR em questão estava tentando habilitar ref this retornos para tipos de referência em oposição a tipos de valor. ref this não pode ser devolvido com segurança de structs; veja aqui o porquê. Portanto, o compromisso de retorno de ref não funcionará.

De qualquer forma, encerrarei este assunto. Abri outro problema aqui: https://github.com/dotnet/corefx/issues/14354

Deve ser capaz de retornar ref "this" de uma postagem de método de extensão de tipo de valor https://github.com/dotnet/roslyn/pull/15650 embora eu assuma C # vNext ...

@benaadams

Deve ser capaz de retornar ref "this" de um método de extensão de tipo de valor post dotnet / roslyn # 15650 embora eu assuma C # vNext ...

Correto. É possível retornar this de um método de extensão ref this . Não é possível retornar this de um método de instância de struct normal. Existem muitos detalhes sangrentos de toda a vida que explicam por que esse é o caso :(

@redknightlois

se quisermos ser restritos, o único hash deve ser uint , pode ser visto como um descuido que o framework retorna int sob essa luz.

Conformidade com CLS? Inteiros sem sinal não são compatíveis com CLS.

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

omajid picture omajid  ·  3Comentários

nalywa picture nalywa  ·  3Comentários

EgorBo picture EgorBo  ·  3Comentários

matty-hall picture matty-hall  ·  3Comentários

jzabroski picture jzabroski  ·  3Comentários