Runtime: Agregar un tipo HashCode para ayudar a combinar códigos hash

Creado en 25 abr. 2016  ·  206Comentarios  ·  Fuente: dotnet/runtime

Reemplazo de la larga discusión con más de 200 comentarios con el nuevo problema dotnet / corefx # 14354

¡Este problema está CERRADO!


Motivación

Java tiene Objects.hash para combinar rápidamente los códigos hash de los campos constituyentes para devolverlos en Object.hashCode() . Desafortunadamente, .NET no tiene tal equivalente y los desarrolladores se ven obligados a lanzar sus propios hashes como este :

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

A veces, la gente incluso recurre a usar Tuple.Create(field1, field2, ...).GetHashCode() para esto, lo cual es malo (obviamente) ya que asigna.

Propuesta

  • Lista de cambios en la propuesta actual (contra la última versión aprobada https://github.com/dotnet/corefx/issues/8034#issuecomment-262331783):

    • Empty propiedad agregada (como punto de partida natural análogo a ImmutableArray )

    • Nombres de argumentos actualizados: 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, (semilla, hash) => semilla.Combine (hash));

var hashCode3 = HashCode.Empty;
foreach (int hash en hash) {hashCode3 = hashCode3.Combine (hash); }
(int) hashCode3;
''

Notas

La implementación debe usar el algoritmo en HashHelpers .

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

Comentario más útil

[@redknightlois] Si lo que necesitamos es una justificación de por qué optar por System , puedo probar una justificación. Creamos HashCode para ayudar en las implementaciones de object.GetHashCode() , parece apropiado que ambos compartan el espacio de nombres.

Esa fue la razón fundamental que @KrzysztofCwalina y yo usamos también. ¡Vendido!

Todos 206 comentarios

Si quieres algo rápido y sucio, puedes usar ValueTuple.Create(field1, field2).GetHashCode() . Es el mismo algoritmo que se usa en Tuple (que, para el caso, es similar al de Objects ) y no tiene la sobrecarga de asignación.

De lo contrario, hay preguntas sobre qué tan bueno será el hash que necesitará, qué valores de campo probables habrá (lo que afecta qué algoritmos darán buenos o malos resultados), si existe la posibilidad de ataques hashDoS, ¿colisiones módulo a binario? los números pares duelen (como lo hacen con las tablas hash binarias pares), y así sucesivamente, lo que hace inaplicable una solución única.

@JonHanna Creo que esas preguntas también se aplican, por ejemplo, a string.GetHashCode() . No veo por qué proporcionar Hash debería ser más difícil que eso.

En realidad, debería ser más simple, ya que los usuarios con requisitos especiales pueden dejar de usar Hash fácilmente, pero dejar de usar string.GetHashCode() es más difícil.

Si quieres algo rápido y sucio, puedes usar ValueTuple.Create (field1, field2) .GetHashCode ().

Ah, buena idea, no había pensado en ValueTuple al hacer esta publicación. Desafortunadamente, no creo que esté disponible hasta C # 7 / la próxima versión del marco, o incluso sé si tendrá ese rendimiento (esas llamadas de propiedad / método a EqualityComparer pueden sumar). Pero no he tomado ningún punto de referencia para medir esto, por lo que realmente no lo sabría. Solo creo que debería haber una clase dedicada / simple para hash que la gente pueda usar sin usar tuplas como una solución alternativa.

De lo contrario, hay preguntas sobre qué tan bueno será el hash que necesitará, qué valores de campo probables habrá (lo que afecta qué algoritmos darán buenos o malos resultados), si existe la posibilidad de ataques hashDoS, ¿colisiones módulo a binario? los números pares duelen (como lo hacen con las tablas hash binarias pares), y así sucesivamente, lo que hace inaplicable una solución única.

Absolutamente de acuerdo, pero no creo que la mayoría de las implementaciones lo tengan en cuenta, por ejemplo, la implementación actual de ArraySegment es bastante ingenua. El propósito principal de esta clase (junto con evitar asignaciones) sería proporcionar una implementación de referencia para las personas que no saben mucho sobre hash, para evitar que hagan algo estúpido como esto . Las personas que necesitan lidiar con las situaciones que describió pueden implementar su propio algoritmo hash.

Desafortunadamente, no creo que esté disponible hasta C # 7 / la próxima versión del marco

Creo que puede usarlo con C # 2, pero no con soporte integrado.

o incluso saber si será tan eficaz (esas llamadas de propiedad / método a EqualityComparer pueden sumarse)

¿Qué haría esta clase de manera diferente? Si llamar explícitamente a obj == null ? 0 : obj.GetHashCode() es más rápido, entonces debe moverse a ValueTuple .

Me hubiera inclinado a hacer +1 en esta propuesta hace un par de semanas, pero estoy menos inclinado a la luz de ValueTuple reducir la sobrecarga de asignación del truco de usar Tuple para esto, esto parece caer entre dos taburetes para mí: si no necesita algo particularmente especializado, puede usar ValueTuple , pero si necesita algo más allá de eso, entonces una clase como esta no irá muy lejos suficiente.

Y cuando tengamos C # 7, tendrá el azúcar sintáctico para hacerlo aún más fácil.

@JonHanna

¿Qué haría esta clase de manera diferente? Si llama explícitamente a obj == null? 0: obj.GetHashCode () es más rápido de lo que debería moverse a ValueTuple.

¿Por qué no tener ValueTuple solo usar la clase Hash para obtener códigos hash? Eso también reduciría significativamente el LOC en el archivo (que en este momento es de aproximadamente ~ 2000 líneas).

editar:

Si no necesita algo particularmente especializado, puede usar ValueTuple

Es cierto, pero el problema es que es posible que muchas personas no se den cuenta de eso e implementen su propia función de hashing ingenua (como la que relacioné anteriormente).

Que de hecho podría quedarme atrás.

Probablemente fuera del alcance de este tema. Pero tener un espacio de nombres hash donde podamos encontrar hashes criptográficos y no criptográficos de alto rendimiento escritos por expertos sería una victoria aquí.

Por ejemplo, tuvimos que codificar xxHash32, xxHash64, Metro128 y también reducir la resolución de 128 a 64 y de 64 a 32 bits nosotros mismos. Tener una variedad de funciones optimizadas puede ayudar a los desarrolladores a evitar escribir sus propias funciones no optimizadas y / o con errores (lo sé, también hemos encontrado algunos errores en el nuestro); pero aún pudiendo elegir dependiendo de las necesidades.

Con mucho gusto donaríamos nuestras implementaciones si hay interés, para que puedan ser revisadas y optimizadas por expertos.

@redknightlois Me complacería agregar mi implementación de SpookyHash a un esfuerzo como ese.

@svick Sin embargo,

@terrajobst , ¿qué tan lejos está esto en

cc: @ellismg

Creo que está listo para revisar en su estado actual.

@mellinoe ¡ Eso es genial! Limpié un poco la propuesta para que sea más concisa y también agregué algunas preguntas al final que creo que deberían abordarse.

@jamesqo También debería basarse en long .

@redknightlois , suena razonable. Actualicé la propuesta para incluir long sobrecargas de Combine .

¿La sugerencia de @JonHanna no es lo suficientemente buena?

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

A menos que haya razones suficientemente buenas por las que eso no sea lo suficientemente bueno, no creemos que esté haciendo el corte.

Más allá de que el código generado sea peor en algunos órdenes de magnitud, no puedo pensar en ninguna otra razón suficientemente buena. A menos que, por supuesto, haya optimizaciones en el nuevo tiempo de ejecución que tengan en cuenta este caso particular, en cuyo caso este análisis es discutible. Habiendo dicho eso, probé esto en 1.0.1.

Permítanme ilustrar con un ejemplo.

Supongamos que tomamos el código real que se está usando para ValueTuple y usamos constantes para llamarlo.

        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();
        }
    }

Ahora, bajo un compilador optimizado, lo más probable es que no debería haber ninguna diferencia, pero en realidad la hay.

Este es el código real para ValueTuple

image
Entonces, ¿qué se puede ver aquí ahora? Primero estamos creando una estructura en la pila, luego llamamos al código hash real.

Ahora compárelo con el uso de HashHelper.Combine que, a todos los efectos, podría ser la implementación real de Hash.Combine

image

¡¡¡Sé!!!
Pero no nos detengamos ahí ... usemos los parámetros reales:

        [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

Lo bueno es que es extremadamente estable. Pero comparémoslo con la alternativa:

image

Ahora vayamos por la borda ...

        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);
        }

Y el resultado es bastante ilustrativo.

image

Realmente no puedo inspeccionar el código real que genera el JIT para la llamada, pero solo el prólogo y el epílogo son suficientes para justificar la inclusión de la propuesta.

image

La conclusión del análisis es simple: que el tipo de tenencia sea struct no significa que sea gratis :)

El rendimiento se mencionó durante la reunión. La pregunta es si es probable que esta API esté en la ruta rápida. Para ser claros, no estoy diciendo que no debamos tener la API. Simplemente digo que, a menos que exista un escenario concreto, es más difícil diseñar la API porque no podemos decir "la necesitamos para X, por lo que la medida del éxito es si X puede usarla". Eso es importante para las API que no le permiten hacer algo nuevo, sino que hacen lo mismo de una manera más optimizada.

Creo que cuanto más importante es tener un hash rápido y de buena calidad, más importante es sintonizar el algoritmo utilizado con el objeto (s) y el rango de valores que es probable que se vean, y por lo tanto, más se necesita. un ayudante, más necesita no usar tal ayudante.

@terrajobst , el rendimiento fue una gran motivación para esta propuesta, pero no la única. Tener un tipo dedicado ayudará con la capacidad de descubrimiento; incluso con el soporte de tuplas integrado en C # 7, los desarrolladores no necesariamente saben que se les equipara por valor. Incluso si lo hacen, pueden olvidar que las tuplas anulan GetHashCode , y probablemente terminarán teniendo que buscar en Google cómo implementar GetHashCode en .NET.

Además, existe un problema de corrección sutil con el uso de ValueTuple.Create.GetHashCode . Pasados ​​8 elementos, solo los últimos 8 elementos tienen hash; el resto se ignora.

@terrajobst En RavenDB GetHashCode, el rendimiento tuvo tal impacto en nuestros resultados que terminamos implementando un conjunto completo de rutinas altamente optimizadas. Incluso Roslyn tiene su propio hash interno https://github.com/dotnet/roslyn/blob/master/src/Compilers/Core/Portable/InternalUtilities/Hash.cs también consulte la discusión sobre Roslyn específicamente aquí: https: // github .com / dotnet / coreclr / issues / 1619 ... Entonces, cuando el rendimiento es CLAVE, no podemos usar la plataforma provista y tenemos que lanzar la nuestra (y pagar las consecuencias).

También el problema de @jamesqo es completamente válido. No he requerido combinar tantos hashes, pero para los casos de 1M, hay alguien que va a cruzar el precipicio con ese.

@JonHanna

Creo que cuanto más importante es tener un hash rápido y de buena calidad, más importante es sintonizar el algoritmo utilizado con el objeto (s) y el rango de valores que es probable que se vean, y por lo tanto, más se necesita. un ayudante, más necesita no usar tal ayudante.

Entonces, ¿está diciendo que agregar una clase de ayuda sería malo, ya que alentaría a las personas a agregar la función de ayuda sin pensar en cómo hacer un hash adecuado?

En realidad, parece que lo contrario sería cierto; Hash.Combine generalmente debería mejorar las implementaciones de GetHashCode . Las personas que saben cómo hacer hash pueden evaluar Hash.Combine para ver si se ajusta a su caso de uso. Los novatos que no saben realmente sobre el hash usarán Hash.Combine lugar de simplemente xor -ing (o peor aún, agregar) los campos constituyentes porque no saben cómo hacer un hash adecuado.

Hablamos de esto un poco más y nos convenciste :-)

Algunas preguntas más:

  1. Necesitamos decidir dónde colocar este tipo. Introducir un nuevo espacio de nombres parece extraño; System.Numerics embargo, System.Collections.Generic también podría funcionar, porque tiene los comparadores y el hash se usa con mayor frecuencia en el contexto de colecciones.
  2. ¿Deberíamos proporcionar un patrón de construcción sin asignación para combinar un número desconocido de códigos hash?

El (2) @Eilon dijo lo siguiente:

Como referencia, ASP.NET Core (y sus predecesores y proyectos relacionados) usan un HashCodeCombiner: https://github.com/aspnet/Common/blob/dev/src/Microsoft.Extensions.HashCodeCombiner.Sources/HashCodeCombiner.cs

( @David Fowler lo mencionó en el hilo de GitHub hace varios meses).

Y este es un ejemplo 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);

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

return hashCodeCombiner;
''

Hablamos de esto un poco más y nos convenciste :-)

🎉

Introducir un nuevo espacio de nombres parece extraño; Sin embargo, System.Numerics podría funcionar.

Si decidimos no agregar un nuevo espacio de nombres, entonces debe tenerse en cuenta que cualquier código que tenga una clase llamada Hash y una directiva using System.Numerics no se compilará con un error de tipo ambiguo.

¿Deberíamos proporcionar un patrón de construcción sin asignación para combinar un número desconocido de códigos hash?

Suena como una buena idea. Como un par de sugerencias iniciales, quizás deberíamos nombrarlo HashBuilder (a la StringBuilder ) y tenerlo return this después de cada método Add para hacerlo más fácil para agregar hashes, así:

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

@jamesqo , actualice la propuesta en la parte superior cuando haya consenso sobre el hilo. Luego podemos hacer la revisión final. Asignándote a ti por ahora mientras manejas el diseño ;-)

Si decidimos no agregar un nuevo espacio de nombres, entonces debe tenerse en cuenta que cualquier código que tenga una clase llamada Hash y una directiva using System.Numerics no se compilará con un error de tipo ambiguo.

Depende del escenario real. En muchos casos, el compilador preferirá su tipo, ya que se recorre la jerarquía de espacio de nombres definida de la unidad de compilación antes de considerar el uso de directivas.

Pero aun así: agregar API puede ser un cambio radical en la fuente. Sin embargo, no es práctico evitar esto, asumiendo que queremos avanzar. 😄 Generalmente nos esforzamos por evitar conflictos, por ejemplo, usando nombres que no son demasiado generales. Por ejemplo, no creo que debamos llamar al tipo Hash . Creo que HashCode probablemente sería mejor.

Como un par de sugerencias iniciales, quizás deberíamos llamarlo HashBuilder

Como primera aproximación, estaba pensando en combinar la estática y el constructor en un solo tipo, así:

`` C #
espacio de nombres System.Collections.Generic
{
estructura pública 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);

así como también:

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

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

return hashCode.Value;
''

¿Pensamientos?

Me gusta la idea de @jamesqo de las llamadas encadenadas (devuelve this de los métodos de instancia Combine ).

Incluso iría tan lejos como para eliminar los métodos estáticos por completo y mantener solo los métodos de instancia ...

Combine(long hashCode) simplemente se reducirá a int . ¿Realmente queremos eso?
¿Cuál es el caso de uso de las sobrecargas de long en primer lugar?

@karelz Por favor, no los elimine, las estructuras no son gratuitas. Los hash se pueden usar en rutas muy calientes, ciertamente no querrá desperdiciar instrucciones cuando el método estático sería esencialmente gratuito. Mire el análisis del código donde mostré el impacto real de la estructura adjunta.

Usamos la clase estática Hashing para evitar conflictos de nombres y el código se ve bien.

@redknightlois Me pregunto si deberíamos esperar el mismo código 'malo' también en un caso de estructura no genérica con un campo int.
Si todavía es un código ensamblador "malo", me pregunto si podríamos mejorar JIT para hacer un mejor trabajo en las optimizaciones aquí. Agregar API solo para guardar un par de instrucciones debería ser nuestro último recurso, en mi opinión.

@redknightlois Curioso, ¿el JIT genera un código peor si la estructura (en este caso HashCode ) puede caber en un registro? Solo será un int grande.

Además, he estado viendo muchas solicitudes de extracción en coreclr recientemente para mejorar el código generado alrededor de las estructuras, y parece que dotnet / coreclr # 8057 habilitará esas optimizaciones. ¿Quizás el código que genera el JIT será mejor después de este cambio?

editar: Veo que @karelz ya ha mencionado mis puntos aquí.

@karelz , estoy de acuerdo con int (que creo que sí, ImmutableArray no tiene gastos generales, por ejemplo), entonces las sobrecargas estáticas son redundante y se puede eliminar.

@terrajobst Algunas ideas más que tengo:

  • Creo que podemos combinar un poco tus y mis ideas. HashCode parece un buen nombre; no tiene que ser una estructura mutable siguiendo el patrón del constructor. En cambio, puede ser una envoltura inmutable alrededor de un int , y cada operación Combine puede devolver un nuevo HashCode . Por ejemplo
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);
  • Deberíamos tener un operador implícito para la conversión a int para que la gente no tenga que tener esa última llamada .Value .
  • Re Combine , ¿es ese el mejor nombre? Suena más descriptivo, pero Add es más corto y más fácil de escribir. ( Mix es otra alternativa, pero es un poco doloroso escribirla).

    • public void Combine(string text, StringComparison comparison) : No creo que realmente pertenezca al mismo tipo, ya que no está relacionado con las cadenas. Además, es bastante fácil escribir StringComparer.XXX.GetHashCode(str) para las raras ocasiones en las que necesita hacerlo.

    • Debemos eliminar las sobrecargas largas de este tipo y tener un tipo HashCode separado para las largas. Algo como Int64HashCode o LongHashCode .

Hice una pequeña implementación de muestra de cosas en TryRoslyn: http://tinyurl.com/zej9yux

Afortunadamente, es fácil de comprobar. Y la buena noticia es que funciona correctamente tal como está 👍

image

Deberíamos tener un operador implícito para la conversión a int para que la gente no tenga que tener esa última llamada .Value.

Probablemente, el código no es tan sencillo, tener una conversión implícita lo limpiaría un poco. Todavía me gusta la idea de poder tener una interfaz de múltiples parámetros también.

        [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, ¿es ese el mejor nombre? Suena más descriptivo, pero Agregar es más corto y más fácil de escribir. (La mezcla es otra alternativa, pero es un poco dolorosa de escribir).

Combine es el nombre real que se usa en la comunidad hash afaik. Y de alguna manera te da una idea clara de lo que está haciendo.

@jamesqo Hay muchas funciones de hash, tuvimos que implementar versiones muy rápidas, desde 32 bits, 64 bits hasta 128 bits para RavenDB (y usamos cada una para diferentes propósitos).

Podemos pensar en este diseño con algún mecanismo extensible 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;
        }

No sé por qué el JIT está creando un código de prólogo tan molesto para esto. No debería, por lo que probablemente se pueda optimizar, deberíamos pedirlo a los desarrolladores de JIT. Pero por lo demás, puede implementar tantos Combinadores diferentes como desee sin desperdiciar una sola instrucción. Dicho esto, este método probablemente sea más útil para funciones hash reales que para combinadores. cc @CarolEidt @AndyAyersMS

EDITAR: Pensando en voz alta aquí para un mecanismo general para combinar funciones de hash criptográficas y no criptográficas en un solo concepto de hash.

@jamesqo

no tiene que ser una estructura mutable siguiendo el patrón del constructor

Ah, sí. En ese caso, estoy bien con ese patrón. Por lo general, no me gusta el patrón de devolver instancias si la operación tuvo un efecto secundario. Es especialmente malo si la API sigue el patrón inmutable WithXxx . Sin embargo, en este caso, el patrón es esencialmente una estructura de datos inmutable, por lo que el patrón funcionaría bien.

Creo que podemos combinar un poco tus y mis ideas.

👍, entonces, ¿qué pasa con:

`` C #
estructura pública 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);
}

tan bien como esto:

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

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

return hashCode.Value;
''

@terrajobst Pensamientos:

  1. Se debe eliminar el método de fábrica Create<T> . De lo contrario, habría 2 formas de escribir lo mismo, HashCode.Create(_val) o new HashCode().Combine(_val) . Además, tener nombres diferentes para Create / Combine no sería compatible con las diferencias, ya que si agregaba un nuevo primer campo, tendría que cambiar 2 líneas.
  2. No creo que la sobrecarga que acepta una cadena / StringComparison pertenezca aquí; HashCode no tiene nada que ver con cadenas. En su lugar, ¿quizás deberíamos agregar una GetHashCode(StringComparison) api a la cadena? (También todas esas son comparaciones ordinales, que es el comportamiento predeterminado de string.GetHashCode ).
  3. ¿Cuál es el punto de tener Value , si ya existe un operador implícito para la conversión a int ? Nuevamente, esto llevaría a que diferentes personas escribieran cosas diferentes.
  4. Tenemos que mover la sobrecarga long a un nuevo tipo. HashCode solo tendrá 32 bits de ancho; no puede caber mucho.
  5. Agreguemos algunas sobrecargas que toman tipos sin firmar, ya que son más comunes en el hash.

Aquí está mi API propuesta:

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);
}

Con solo estos métodos, el ejemplo de ASP.NET aún se puede escribir 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

¿Cuál es el punto de tener Value , si ya existe un operador implícito para la conversión a int ? Nuevamente, esto llevaría a que diferentes personas escribieran cosas diferentes.

Las Directrices de diseño del marco para las sobrecargas del operador dicen:

CONSIDERE proporcionar métodos con nombres descriptivos que correspondan a cada operador sobrecargado.

Muchos idiomas no admiten la sobrecarga de operadores. Por esta razón, se recomienda que los tipos que sobrecarguen a los operadores incluyan un método secundario con un nombre específico de dominio apropiado que proporcione una funcionalidad equivalente.

Específicamente, F # es uno de los lenguajes que dificulta la invocación de operadores de conversión implícitos.


Además, no creo que tener una sola forma de hacer las cosas sea tan importante. En mi opinión, es más importante hacer que la API sea conveniente. Si solo quiero combinar códigos hash de pocos valores, creo que HashCode.CombineHashCodes(value1, value2, value3) es más simple, más corto y más fácil de entender que new HashCode().Combine(value1).Combine(value2).Combine(value3) .

La API del método de instancia sigue siendo útil para casos más complicados, pero creo que el caso más común debería tener la API de método estático más simple.

@svick , su punto sobre otros lenguajes que no admiten operadores es legítimo. Cedo, agreguemos Value entonces.

No creo que tener una sola forma de hacer las cosas sea tan importante.

Es importante. Si alguien lo hace de una manera, y lee el código de una persona que lo hace de otra manera, entonces tendrá que buscar en Google lo que hace al revés.

Si solo quiero combinar códigos hash de pocos valores, creo que HashCode.CombineHashCodes (value1, value2, value3) es más simple, más corto y más fácil de entender que el nuevo HashCode (). Combine (value1) .Combine (value2) .Combine ( valor3).

  • El problema con un método estático es que, dado que no habrá una sobrecarga de params int[] , tendremos que agregar sobrecargas para cada aridad diferente, que es mucho menos rentable. Es mucho mejor tener un método que cubra todos los casos de uso.
  • El segundo formulario será fácil de entender una vez que lo vea una o dos veces. De hecho, podría argumentar que es más legible, ya que es más fácil encadenar verticalmente (y por lo tanto minimiza las diferencias cuando se agrega / elimina un campo):
public override int GetHashCode()
{
    return new HashCode()
        .Combine(_field1)
        .Combine(_field2)
        .Combine(_field3)
        .Combine(_field4);
}

[@svick] No creo que tener una sola forma de hacer las cosas sea tan importante.

Creo que es importante minimizar la cantidad de formas en que puede hacer lo mismo porque evita confusiones. Al mismo tiempo, nuestro objetivo no es ser 100% libre de superposición si ayuda a alcanzar otros objetivos, como la visibilidad, la conveniencia, el rendimiento o la legibilidad. En general, nuestro objetivo es minimizar los conceptos, en lugar de las API. Por ejemplo, múltiples sobrecargas son menos problemáticas que tener múltiples métodos diferentes con terminología inconexa.

La razón por la que agregué el método de fábrica es dejar en claro cómo se obtiene un código hash inicial. Crear la estructura vacía seguida de Combine no parece muy intuitivo. Lo lógico sería agregar .ctor pero para evitar el boxing tendría que ser genérico, lo cual no se puede hacer con un .ctor. Un método de fábrica genérico es la mejor opción.

Un buen efecto secundario es que se ve muy similar a cómo se ven las estructuras de datos inmutables en el marco. Y en el diseño de API, favorecemos fuertemente la coherencia sobre casi cualquier otra cosa.

[@svick] Si solo quiero combinar códigos hash de pocos valores, creo que HashCode.CombineHashCodes (value1, value2, value3) es más simple, más corto y más fácil de entender que el nuevo HashCode (). Combine (value1) .Combine (value2 ) .Combinar (valor3).

Estoy de acuerdo con @jamesqo : lo que me gusta del patrón de construcción es que escala a una cantidad arbitraria de argumentos con una penalización mínima en el rendimiento (si corresponde, dependiendo de qué tan bueno sea nuestro inliner).

[@jamesqo] No creo que la sobrecarga de aceptar una cadena / StringComparison pertenezca aquí; HashCode no tiene nada que ver con cadenas

Punto justo. Lo agregué porque estaba referenciado en el código de @Eilon . Por experiencia, diría que las cadenas son muy comunes. Por otro lado, no estoy seguro de que especificar una comparación sea. Dejémoslo por ahora.

[@jamesqo] Tenemos que mover la sobrecarga larga a un nuevo tipo. HashCode solo tendrá 32 bits de ancho; no puede caber mucho.

Ese es un buen punto. ¿Necesitamos siquiera una versión long ? Solo lo dejé porque se mencionó anteriormente y realmente no lo pensé.

Ahora que lo estoy, parece que deberíamos dejar solo 32 bits porque de eso se trata .NET GetHashCode() . En ese sentido, ni siquiera estoy seguro de que debamos agregar la versión uint . Si usa hash fuera de ese ámbito, creo que está bien señalar a las personas los algoritmos de hash de propósito más general que tenemos en System.Security.Cryptography .

`` C #
estructura pública 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);

}
''

Ahora que lo estoy, parece que deberíamos dejar solo 32 bits porque de eso se trata .NET GetHashCode (). En ese sentido, ni siquiera estoy seguro de que debamos agregar la versión uint. Si usa hash fuera de ese ámbito, creo que está bien señalar a las personas los algoritmos de hash de propósito más general que tenemos en System.Security.Cryptography.

@terrajobst Hay muy diferentes tipos de algoritmos hash, un zoológico real. De hecho, probablemente el 70% no son criptográficos por diseño. Y probablemente más de la mitad de ellos están diseñados para trabajar con más de 64 bits (el objetivo común es 128/256). Apuesto a que el marco decidió usar 32 bits (no he estado allí) se debe a que en ese momento x86 todavía era un gran consumidor y los hashes se usan en todas partes, por lo que el rendimiento en hardware menor era primordial.

Además, para ser estrictos, la mayoría de las funciones hash están realmente definidas en el dominio uint , y no en el int porque las reglas de cambio son diferentes. De hecho, si verifica el código que publiqué antes, el int se convierte inmediatamente en un uint debido a eso (y usa la optimización ror/rol ). Por si acaso, si queremos ser estrictos, el único hash debería ser uint , se puede ver como un descuido que el marco devuelva int bajo esa luz.

Restringir esto a int no es mejor que lo que tenemos hoy. Si fuera mi decisión, presionaría al equipo de diseño para que investigara cómo podríamos acomodar el soporte de 128 y 256 variantes y diferentes funciones hash (incluso si arrojáramos una alternativa de no me hagas pensar debajo de tus huellas dactilares).

Los problemas causados ​​por la simplificación excesiva son a veces peores que los problemas de diseño que se presentan cuando se les obliga a lidiar con cosas complejas. Simplificar la funcionalidad en un grado tan grande porque se percibe a los desarrolladores como not being able to deal with having multiple options puede conducir fácilmente a la ruta del estado actual de SIMD. La mayoría de los desarrolladores preocupados por el rendimiento no pueden usarlo, y todos los demás tampoco lo usarán porque la mayoría no se ocupa de aplicaciones sensibles al rendimiento que tienen objetivos de rendimiento tan precisos de todos modos.

El caso del hash es similar, los dominios donde usarías 32 bits están muy restringidos (la mayoría ya están cubiertos por el propio framework), para el resto no tienes suerte.

image

Además, tan pronto como tenga que lidiar con más de 75000 elementos, tiene un 50% de posibilidades de tener una colisión, y eso es malo en la mayoría de los escenarios (y eso suponiendo que tenga una función hash bien diseñada). Es por eso que 64 bits y 128 bits se usan tanto fuera de los límites de las estructuras de tiempo de ejecución.

Con un diseño pegado en int , solo cubrimos los problemas causados ​​por no tener el periódico de los lunes en 2000 (por lo que ahora todos escriben su pobre hash por sí mismos) pero no avanzaremos ni un paso en el estado del arte tampoco.

Esos son mis dos centavos en la discusión.

@redknightlois , creo que entendemos las limitaciones de los hash int. Pero estoy de acuerdo con @terrajobst : esta función debería tratar las API para calcular hashes con el fin de devolverlos de las anulaciones de Object.GetHashCode. Además, podríamos tener una biblioteca separada para hash más moderno, pero yo diría que debería ser una discusión separada, ya que debe incluir decidir qué hacer con Object.GetHashCode y todas las estructuras de datos hash existentes.

A menos que crea que todavía es beneficioso hacer una combinación de hash en 128 bits y luego convertir a int para que el resultado se pueda devolver desde GetHahsCode.

@KrzysztofCwalina Estoy de acuerdo en que son dos enfoques diferentes. Uno es solucionar un problema causado en 2000; otra diferente es abordar el problema general de hash. Si todos estamos de acuerdo en que esta es una solución para lo primero, la discusión habrá terminado. Sin embargo, para una discusión de diseño para un hito "Futuro", tengo la sensación de que no será suficiente, principalmente porque lo que haremos aquí tendrá un impacto en la discusión futura. Cometer errores aquí tendrá un impacto.

@redknightlois , yo propondría lo siguiente:

@redknightlois

Cometer errores aquí tendrá un impacto.

Creo que si en el futuro queremos admitir escenarios más avanzados, entonces podemos hacerlo en un tipo separado de HashCode . Las decisiones aquí no deberían afectar realmente esos casos.

Creé un problema diferente para comenzar a abordar eso.

@redknightlois : +1 :. Por cierto, respondiste antes de que pudiera editar mi comentario, pero en realidad probé tu idea (arriba) de hacer que el hash funcione con cualquier tipo (int, long, decimal, etc.) y encapsular la lógica de hash central en una estructura: https://github.com/jamesqo/HashApi (el uso de muestra estaba aquí ). Pero, tener dos parámetros de tipo genérico terminó siendo demasiado complejo, y la inferencia del tipo de compilador terminó no funcionando cuando intenté usar la API. Entonces, sí, es una buena idea convertir el hash más avanzado en un tema separado por ahora.

@terrajobst La API parece casi lista, pero hay 1 o 2 cosas más que me gustaría cambiar.

  • Inicialmente no quería el método de fábrica estático, ya que HashCode.Create(x) tiene el mismo efecto que new HashCode().Combine(x) . Pero he cambiado de opinión sobre eso ya que eso significa 1 hash extra. En su lugar, ¿por qué no cambiamos el nombre de Create a Combine ? Parece un poco molesto tener que escribir una cosa para el primer campo y otra para el segundo campo.
  • Creo que deberíamos tener HashCode implementar IEquatable<HashCode> e implementar algunos de los operadores de igualdad. No dude en avisarme si tiene alguna objeción.

(Con suerte) propuesta 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 dijo:

Punto justo. Lo agregué porque estaba referenciado en el código de @Eilon . Por experiencia, diría que las cadenas son muy comunes. Por otro lado, no estoy seguro de que especificar una comparación sea. Dejémoslo por ahora.

En realidad, es muy importante: crear hashes para cadenas a menudo implica tener en cuenta el propósito de esa cadena, que involucra tanto su cultura como su distinción entre mayúsculas y minúsculas. StringComparer no se trata de comparaciones per se, sino de proporcionar implementaciones específicas de GetHashCode que son conscientes de la cultura / caso.

Sin esta API, tendrías que hacer algo extraño como:

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

Y eso está repleto de asignaciones, sigue patrones de sensibilidad cultural deficientes, etc.

@Eilon, en tal caso, esperaría que el código llamara explícitamente a string.GetHashCode(StringComparison comparison) que es consciente de la cultura / caso y pasa el resultado como int a Combine .

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

@Eilon , podrías usar StringComparer.InvariantCultureIgnoreCase.GetHashCode.

Esos son ciertamente mejores en términos de asignaciones, pero esas llamadas no son agradables a la vista ... Tenemos usos en todo ASP.NET donde los hashes deben incluir cadenas de cultura / caso.

Bastante justo, combinando todo lo que se dijo anteriormente, ¿qué tal esta forma entonces?

`` C #
espacio de nombres System.Collections.Generic
{
estructura pública HashCode: IEquatable
{
Public static HashCode Combine (int hash);
combinación de HashCode estático público(T obj);
public static HashCode Combine (texto de cadena, comparación 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)
.Combinar (_field2)
.Combinar (_field3)
.Combinar (_field4);
}
''

¡envíalo! :-)

@terrajobst _Hold on --_ ¿no se puede implementar Combine(string, StringComparison) como un método de extensión?

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:
                ...
        }
    }
}

Preferiría mucho, mucho, que fuera un método de extensión en lugar de una parte de la firma de tipo. Sin embargo, si usted o @Elion piensan absolutamente que este debería ser un método incorporado, no bloquearé esta propuesta.

( editar: también System.Numerics es probablemente un mejor espacio de nombres, a menos que tengamos tipos relacionados con hash en Collections.Generic hoy que no conozco).

LGTM. Yo iría a la extensión.

Sí, podría ser un método de extensión, pero ¿qué problema resuelve?

@terrajobst

Sí, podría ser un método de extensión, pero ¿qué problema resuelve?

Estaba sugiriendo en código ASP.NET. Si es común para su caso de uso, está bien, pero puede que no sea cierto para otras bibliotecas / aplicaciones. Si resulta que esto es lo suficientemente común más adelante, siempre podríamos reevaluar y decidir agregarlo en una propuesta separada.

Mhhh esto es fundamental de todos modos. Una vez definido, será parte de la firma de todos modos. Desecha el comentario. Está bien como está.

El uso de métodos de extensión es útil para los casos en los que:

  1. es un tipo existente que nos gustaría aumentar sin tener que enviar una actualización al tipo en sí
  2. resolver problemas de capas
  3. separe las API supercomunes de las API mucho menos utilizadas.

No creo que (1) o (2) se apliquen aquí. (3) solo ayudaría si moviéramos el código a un ensamblado diferente de HashCode o si lo moviéramos a un espacio de nombres diferente. Yo diría que las cadenas son lo suficientemente comunes como para que no valga la pena. De hecho, incluso diría que son tan comunes que tratarlos como de primera clase tiene más sentido que tratar de separarlos artificialmente en un tipo de extensión.

@terrajobst , para que quede claro, estaba sugiriendo string completo y dejar que ASP.NET escriba su propio método de extensión para cadenas.

Yo diría que las cadenas son lo suficientemente comunes como para que no valga la pena. De hecho, incluso diría que son tan comunes que tratarlos como de primera clase tiene más sentido que tratar de separarlos artificialmente en un tipo de extensión.

Sí, pero ¿qué tan común es que alguien quiera obtener el código hash no ordinal de una cadena, que es el único escenario del que no se ocupa la sobrecarga Combine<T> existente? (por ejemplo, ¿alguien que llama a StringComparer.CurrentCulture.GetHashCode en sus anulaciones?) Puede que me equivoque, pero no he visto muchos.

Perdón por el rechazo a esto; es solo que una vez que se agrega una API, no hay vuelta atrás.

sí, pero ¿qué tan común es que alguien quiera obtener el código hash no ordinal de una cadena?

Puede que esté sesgado, pero la invariancia de mayúsculas y minúsculas es bastante popular. Claro, no muchos (si los hay) se preocupan por los códigos hash específicos de la cultura, pero puedo ver los códigos hash que ignoran la carcasa, y eso parece lo que @Eilon está StringComparison.OrdinalIgnoreCase ).

Perdón por el rechazo a esto; es solo que una vez que se agrega una API, no hay vuelta atrás.

No es broma 😈 De acuerdo, pero incluso si la API no se usa tanto, es útil y no causa ningún daño.

@terrajobst De acuerdo, agreguemos: +1: Último problema: mencioné esto anteriormente, pero ¿podemos hacer que el espacio de nombres sea Numérico en lugar de Colecciones.Generico? Si tuviéramos que agregar más tipos relacionados con el hash en el futuro, como sugiere @redknightlois , creo que serían un nombre inapropiado en Colecciones.

Me encanta. 🍔

No creo que Hashing caiga conceptualmente en Colecciones. ¿Qué pasa con System.Runtime?

Iba a sugerir lo mismo, o incluso System. Tampoco es numérica.

@karelz , System.Runtime podría funcionar. El sistema

No deberíamos ponerlo en System.Runtime ya que es para casos esotéricos y bastante especializados. Hablé con @KrzysztofCwalina y ambos pensamos que es uno de los dos:

  • System
  • System.Collections.*

Ambos nos inclinamos por System .

Si lo que necesitamos es una justificación de por qué optar por System , puedo probar una justificación. Creamos HashCode para ayudar en las implementaciones de object.GetHashCode() , parece apropiado que ambos compartan el espacio de nombres.

@terrajobst Creo que System debería ser el espacio de nombres, entonces. Vamos a: enviarlo:

Se actualizó la especificación de API en la descripción.

[@redknightlois] Si lo que necesitamos es una justificación de por qué optar por System , puedo probar una justificación. Creamos HashCode para ayudar en las implementaciones de object.GetHashCode() , parece apropiado que ambos compartan el espacio de nombres.

Esa fue la razón fundamental que @KrzysztofCwalina y yo usamos también. ¡Vendido!

@jamesqo

Supongo que también quiere proporcionarle al RP la implementación.

@terrajobst Sí, definitivamente. Gracias por tomarse el tiempo para revisar esto.

Sí definitivamente.

Dulce. En ese caso te lo dejo asignado. ¿Eso está bien contigo @karelz?

Gracias por tomarse el tiempo para revisar esto.

Gracias por tomarse el tiempo de trabajar con nosotros en la forma API. Puede ser un proceso doloroso ir y venir. ¡Apreciamos mucho tu paciencia!

Y estoy deseando eliminar la implementación de ASP.NET Core y usar esto en su lugar 😄

public static HashCode Combine (texto de cadena, comparación de StringComparison);
public HashCode Combine (texto de cadena, comparación de StringComparison);

Nit: Los métodos en String que toman StringComparison (por ejemplo, Equals , Compare , StartsWith , EndsWith , etc. .) use comparisonType como nombre del parámetro, no comparison . ¿El parámetro debería llamarse comparisonType aquí también para ser coherente?

@justinvp , eso parece más una falla de nomenclatura en los métodos de String; Type es redundante. No creo que debamos hacer que los nombres de los parámetros en las nuevas API sean más detallados solo para "seguir el precedente" con los anteriores.

Como otro punto de datos, xUnit eligió usar comparisonType también.

@justinvp Me has convencido. Ahora que lo pienso intuitivamente, "no distingue entre mayúsculas y minúsculas" o "dependiente de la cultura" es un "tipo" de comparación. Cambiaré el nombre.

Estoy de acuerdo con la forma de esto, pero con respecto a StringComparison, una posible alternativa:

No incluya:

`` C #
public static HashCode Combine (texto de cadena, comparación de StringComparison);
public HashCode Combine (texto de cadena, comparación de StringComparison);

Instead, add a method:

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

Entonces, en lugar de escribir:

`` C #
public override int GetHashCode ()
{
return HashCode.Combine (_field1)
.Combinar (_field2)
.Combinar (_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));
}

Sí, es un poco más largo, pero resuelve el mismo problema sin necesidad de dos métodos especializados en HashCode (que acabamos de promocionar a Sistema), y obtienes un método auxiliar estático que se puede usar en otras situaciones no relacionadas. También lo mantiene similar a cómo lo usaría si ya tiene un StringComparer (ya que no estamos hablando de sobrecargas del comparador):

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

@stephentoub , FromComparison parece una buena idea. De hecho, propuse hacia arriba en el hilo para agregar una string.GetHashCode(StringComparison) api, lo que hace que su ejemplo sea aún más simple (asumiendo una cadena no nula):

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

Sin embargo,

(editar: hizo una propuesta para su api).

Tampoco me gusta agregar 2 métodos especializados en HashCode para cadena.
@Eilon mencionaste que el patrón se usa en ASP.NET Core. ¿Cuánto crees que lo usarán los desarrolladores externos?

@jamesqo ¡ gracias por impulsar el diseño! Como dijo @terrajobst , agradecemos su ayuda y paciencia. Las pequeñas API básicas a veces pueden tardar un tiempo en iterarse :).

Veamos dónde aterrizamos con este último comentario de API, luego podemos seguir adelante con la implementación.

¿Debería haber un:

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

?

(Disculpas si eso ya fue descartado y me lo pierdo aquí).

@stephentoub dijo:

escribir:

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

Sí, es un poco más largo, pero resuelve el mismo problema sin necesidad de dos métodos especializados en HashCode (que acabamos de promocionar a Sistema), y obtienes un método auxiliar estático que se puede usar en otras situaciones no relacionadas. También lo mantiene similar a cómo lo usaría si ya tiene un StringComparer (ya que no estamos hablando de sobrecargas del comparador):


Bueno, no es solo un poco más, es muy largo y tiene una capacidad de descubrimiento cero.

¿Cuál es la resistencia a agregar este método? Si es útil, se puede implementar claramente correctamente, no tiene ambigüedad en lo que hace, ¿por qué no agregarlo?

Tener el método de conversión / ayuda estática adicional está bien, aunque no estoy seguro de si lo usaría, pero ¿por qué a expensas de los métodos de conveniencia?

¿Por qué a expensas de los métodos de conveniencia?

Porque no me queda claro que los métodos de conveniencia sean realmente necesarios aquí. Entiendo que ASP.NET lo hace en varios lugares. Cuantos lugares ¿Y en cuántos de esos lugares es realmente una variable StringComparison que tiene en lugar de un valor conocido? En cuyo caso ni siquiera necesita el ayudante que mencioné y podría hacer:

`` 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);

y en realidad es más rápido, ya que no tenemos que ramificarnos dentro de Combine para hacer exactamente lo mismo que el desarrollador podría haber escrito. ¿Es el código adicional un inconveniente tan grande que vale la pena agregar sobrecargas especializadas para ese caso? ¿Por qué no sobrecargas para StringComparer? ¿Por qué no sobrecargas para EqualityComparer? ¿Por qué no las sobrecargas que toman Func<T, int> ? En algún momento, traza la línea y dice "el valor que proporciona esta sobrecarga simplemente no vale la pena", porque todo lo que agregamos tiene un costo, ya sea el costo de mantenimiento, el costo del tamaño del código, el costo de lo que sea , y si el desarrollador realmente necesita este caso, el desarrollador tiene muy poco código adicional para manejar con menos casos especializados. Así que estaba sugiriendo que tal vez el lugar correcto para trazar la línea sea antes de estas sobrecargas en lugar de después (pero como dije al comienzo de mi respuesta anterior, "Estoy de acuerdo con la forma de esto", y estaba sugiriendo una alternativa). .

Aquí está la búsqueda que hice: https://github.com/search?p=2&q=user%3Aaspnet+hashcodecombiner&type=Code&utf8=%E2%9C%93

De ~ 100 coincidencias, incluso solo de las primeras páginas, casi todos los casos de uso tienen cadenas y, en varios casos, se utilizan diferentes tipos de comparaciones de cadenas:

  1. Ordinal: https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/Tagr.HelperAtparete#criptor
  2. Ordinal + IgnoreCase: https://github.com/aspnet/Razor/blob/bdbb854bdbde260b3c70f565a93ebbb185a7c5a7/src/Microsoft.AspNetCore.Razor/Compilation/TagHelpers/TagComHelperRequiredtor49
  3. Ordinal: https://github.com/aspnet/Razor/blob/bdbb854bdbde260b3c70f565a93ebbb185a7c5a7/src/Microsoft.AspNetCore.Razor/Chunks/Generators/AttributeBlockChunkLenerator.cs#
  4. Ordinal: https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/Tagr.HelperDesignTimeDescriptor41
  5. Ordinal: https://github.com/aspnet/Razor/blob/dbcb6901209859e471c9aa978912cf7d6c178668/src/Microsoft.AspNetCore.Razor.Evolution/Legacy/AttributeBlockChunkGenerator.cs#
  6. Ordinal: https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/CaseSensitiveTagComHelperDescriptor
  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/Symbols/SymbolBase.cs#L52
  9. Ordinal: https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/CaseSensitiveTagteHpareperAcsttribute
  10. Ordinal: https://github.com/aspnet/Razor/blob/77ed9f22fc8894fbce796bb8a704d6cd03a3b226/src/Microsoft.AspNetCore.Razor.TagHelpers.Testing.Sources/TagHelperAttributeDespareignTimer.

(Y decenas de personas más).

Entonces parece que ciertamente dentro del código base de ASP.NET Core, este es un patrón extremadamente común. Por supuesto que no puedo hablar con ningún otro sistema.

De ~ 100 partidos

Cada uno de los 10 que enumeró (no miré el resto de la búsqueda) especifica explícitamente la comparación de cadenas, en lugar de extraerla de una variable, por lo que no estamos hablando solo de la diferencia entre, por ejemplo:

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

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

? Eso no es "mucho más largo" y es más eficiente, a menos que me esté perdiendo algo.

De todos modos, como he dicho, simplemente estoy sugiriendo que realmente consideremos si estas sobrecargas son necesarias. Si la mayoría de la gente cree que sí, y no solo estamos considerando nuestra propia base de código ASP.NET, está bien.

Relacionado, ¿cuál es el comportamiento que estamos planeando para entradas nulas? ¿Qué pasa con int == 0? Puedo comenzar a ver más beneficios en la sobrecarga de cadenas si permitimos que se pase nulo, ya que creo que StringComparer.GetHashCode generalmente arroja una entrada nula, por lo que si esto realmente es común, comienza a volverse más engorroso si la persona que llama tiene a casos especiales nulos. Pero eso también plantea la pregunta de cuál será el comportamiento cuando se proporcione un valor nulo. ¿Está un 0 mezclado con el código hash como con cualquier otro valor? ¿Se trata como un nop y el código hash se deja solo?

Creo que el mejor enfoque general para nulo es mezclar en un cero. Para un solo elemento nulo agregado, tenerlo como nop sería mejor, pero si alguien está alimentando en una secuencia, se vuelve más beneficioso tener 10 nulos hash de manera diferente a 20.

De hecho, mi voto proviene de la perspectiva del código base de ASP.NET Core, donde sería muy útil tener una sobrecarga que tenga en cuenta las cadenas. Las cosas sobre la longitud de la línea no eran realmente mi principal preocupación, sino más bien sobre la capacidad de descubrimiento.

Si una sobrecarga consciente de cadenas no estuviera disponible en el sistema, simplemente agregaríamos un método de extensión interno en ASP.NET Core y lo usaríamos.

Si una sobrecarga consciente de cadenas no estuviera disponible en el sistema, simplemente agregaríamos un método de extensión interno en ASP.NET Core y lo usaríamos.

Creo que sería una gran solución por ahora, hasta que veamos más evidencia de que dicha API es necesaria en general, también fuera de la base de código ASP.NET Core.

Debo decir que no veo el valor de eliminar la sobrecarga string . No reduce la complejidad, no hace que el código sea más eficiente y no nos impide mejorar otras áreas, como proporcionar un método que devuelva un StringComparer de un StringComparison . El azúcar sintáctico _es_ importa, porque .NET siempre se ha tratado de facilitar el caso común. También queremos guiar al desarrollador para que haga lo correcto y caiga en el pozo del éxito.

Debemos reconocer que las cuerdas son especiales e increíblemente comunes. Añadiendo una sobrecarga que los especialice conseguimos dos cosas:

  1. Hacemos escenarios como el de @Eilon mucho más fáciles.
  2. Hacemos que sea reconocible que considerar la comparación de cuerdas es importante, especialmente la carcasa.

También debemos considerar que los ayudantes repetitivos comunes como el método de extensión @Eilon mencionado anteriormente no son algo bueno, es algo malo. Da lugar a horas desperdiciadas de copiar y pegar métodos auxiliares y probablemente resultará en una saturación de código innecesaria y errores cuando no se hace correctamente.

Sin embargo, si la principal preocupación es la carcasa especial string , ¿qué tal esto?

`` C #
estructura pública HashCode: IEquatable
{
combinación de HashCode público(T obj, IEqualityComparercomparador);
}

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

@terrajobst , su compromiso es inteligente. Me gusta cómo ya no tienes que llamar a GetHashCode explícitamente o anidar un conjunto adicional de paréntesis con un comparador personalizado.

(editar: ¿Supongo que debería darle crédito a @JonHanna ya que lo mencionó antes en el hilo? 😄)

@JonHanna Sí, también vamos a codificar las entradas nulas como 0.

Perdón por interrumpir la conversación aquí. Pero, ¿dónde debería poner el nuevo tipo? @mellinoe @ericstj @weshaggard , ¿me sugiere que haga un nuevo ensamblado / paquete para este tipo como System.HashCode , o debería agregarlo a un ensamblado existente como System.Runtime.Extensions ? Gracias.

Recientemente hemos refactorizado bastante el diseño del ensamblaje en .NET Core; Sugiero ponerlo donde viven los comparadores de concreto, que parecen indicar System.Runtime.Extensions .

@weshaggard?

@terrajobst Con respecto a la propuesta en sí, acabo de descubrir que no podemos nombrar las sobrecargas estáticas y de instancia Combine , desafortunadamente. 😢

Lo siguiente da como resultado un error del compilador porque la instancia y los métodos estáticos no pueden tener los mismos nombres:

using System;
using System.Collections.Generic;

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

    public static void Combine(int i)
    {
    }
}

Ahora tenemos 2 opciones:

  • Cambie el nombre de las sobrecargas estáticas a algo diferente como Create , Seed , etc.
  • Mueva las sobrecargas estáticas a otra clase 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);

Soy preferencial hacia el segundo. Es lamentable que tengamos que solucionar este problema, pero ... ¿pensamientos?

Separar la lógica en 2 tipos me suena extraño: para usar HashCode tienes que hacer la conexión y comenzar con Hash class en su lugar.

Prefiero agregar el método Create (o Seed o Init ).
También agregaría la sobrecarga sin argumentos HashCode.Create().Combine(_field1).Combine(_field2) .

@karelz , no creo que debamos agregar un método de fábrica si no es el mismo nombre. Deberíamos ofrecer el constructor sin parámetros, new , ya que es más natural. Además, no podemos evitar que la gente escriba new HashCode().Combine ya que es una estructura.

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

Esto hace una combinación adicional con el código hash de 0 y _field1 , en lugar de inicializar directamente desde el código hash. Sin embargo, un efecto secundario del hash actual que estamos usando es que se pasa 0 como primer parámetro, se rotará a cero y se agregará a cero. Y cuando se xo 0 con el primer código hash, solo producirá el primer código hash. Entonces, si el JIT es bueno para el plegado constante (y creo que optimiza este xor), en efecto, esto debería ser equivalente a la inicialización directa.

API propuesta (especificación actualizada):

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 , ¿tiene una opinión sobre un método de fábrica frente al uso del constructor predeterminado? Descubrí que el compilador no permite una sobrecarga Combine estática ya que entra en conflicto con los métodos de instancia, por lo que tenemos la opción de

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

// or, using default constructor

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

La ventaja del primero es que es un poco más complejo. La ventaja del segundo es que tendrá nombres consistentes para que no tenga que escribir algo diferente para el primer campo.

Otra posibilidad son dos tipos diferentes, uno con la fábrica Combine , otro con la instancia Combine (o el segundo como una extensión del primer tipo).

No estoy seguro de cuál preferiría TBH.

@JonHanna , tu segunda idea con las sobrecargas de instancias como métodos de extensión suena genial. Dicho esto, hc.Combine(obj) en ese caso intenta captar la sobrecarga estática: TryRoslyn .

Propuse tener una clase estática como punto de entrada algunos comentarios anteriores, lo que me recuerda ... @karelz , dijiste

Separar la lógica en 2 tipos me suena extraño: para usar HashCode, debe hacer la conexión y comenzar con la clase Hash en su lugar.

¿Qué conexión tendría que hacer la gente? ¿No les presentaríamos Hash primero, y luego, desde allí, pueden llegar a HashCode ? No creo que agregar una nueva clase estática sea un problema.

Separar la lógica en 2 tipos me suena extraño: para usar HashCode, debe hacer la conexión y comenzar con la clase Hash en su lugar.

Podríamos mantener el tipo de nivel superior HashCode y simplemente anidar la estructura. Esto permitiría el uso deseado manteniendo el "punto de entrada" de la API en un tipo de nivel superior, por ejemplo:

`` c #
sistema de espacio de nombres
{
clase estática pública HashCode
{
public static HashCodeValue Combine (int hash);
combinación de HashCodeValue estática pública(T obj);
combinación de HashCodeValue estática pública(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();
    }
}

}
''

Editar: Aunque, probablemente necesite un nombre mejor que HashCodeValue para el tipo anidado si seguimos esta ruta ya que HashCodeValue.Value es un poco redundante, no es que Value se usaría mucho a menudo. Tal vez ni siquiera necesitemos una propiedad Value ; puede obtener Value través de GetHashCode() si no desea enviar a int .

@justinvp ¿Cuál es el problema de tener dos tipos separados en primer lugar? Este sistema parece funcionar bien para LinkedList<T> y LinkedListNode<T> , por ejemplo.

Sin embargo, ¿cuál es el problema de tener dos tipos separados en primer lugar?

Hay dos preocupaciones con dos tipos de nivel superior:

  1. ¿Qué tipo es el "punto de entrada" para la API? Si los nombres son Hash y HashCode , ¿con cuál empiezas? No está claro a partir de esos nombres. Con LinkedList<T> y LinkedListNode<T> está bastante claro cuál es el punto de entrada principal, LinkedList<T> , y cuál es un ayudante.
  2. Contaminando el espacio System nombres System nombres

El anidamiento ayuda a mitigar estas preocupaciones.

@justinvp

¿Qué tipo es el "punto de entrada" para la API? Si los nombres son Hash y HashCode, ¿con cuál empezar? No está claro a partir de esos nombres. Con LinkedListy LinkedListNodeestá bastante claro cuál es el punto de entrada principal, LinkedList, y que es un ayudante.

Bien, bastante justo. ¿Qué pasa si nombramos los tipos Hash y HashValue , no tipos de anidamiento? ¿Eso denotaría suficiente relación de subyugación entre los dos tipos?

Si lo hacemos, entonces el método de fábrica se vuelve aún más escueto: Hash.Combine(field1).Combine(field2) . Además, usar el tipo de estructura en sí mismo sigue siendo práctico. Por ejemplo, alguien puede querer recopilar una lista de hashes y comunicar esto al lector se usa un List<HashValue> lugar de un List<int> . Esto podría no funcionar tan bien si hiciéramos el tipo anidado: List<HashCode.HashCodeValue> (incluso List<Hash.Value> es un poco confuso a primera vista).

Contaminando el espacio de nombres del sistema. No es tan preocupante como (1), pero es algo a tener en cuenta cuando consideramos exponer una nueva funcionalidad en el espacio de nombres del sistema.

Estoy de acuerdo, pero también creo que es importante que sigamos las convenciones y no sacrifiquemos la facilidad de uso. Por ejemplo, las únicas API de BCL en las que puedo pensar donde tenemos tipos anidados (las colecciones inmutables no cuentan, no son estrictamente parte del marco) es List<T>.Enumerator , donde queremos ocultar activamente los tipos anidados type porque está destinado al uso del compilador. No queremos hacer eso en este caso.

Tal vez ni siquiera necesitemos una propiedad de Valor; puede obtener el Valor a través de GetHashCode () si no desea convertir a int.

Pensé en eso antes. Pero entonces, ¿cómo va a saber el usuario que el tipo anula GetHashCode , o que tiene un operador implícito?

API propuesta

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();
}

¿Qué pasa si nombramos los tipos Hash y HashValue, no tipos de anidamiento?

Hash parece un nombre demasiado general. Creo que necesitamos tener HashCode en el nombre de la API de punto de entrada porque su propósito es ayudar a implementar GetHashCode() , no GetHash() .

alguien puede querer recopilar una lista de hashes y comunicar esto al lector una listase usa en lugar de una lista. Esto podría no funcionar tan bien si hiciéramos el tipo anidado: List(incluso Listaes un poco confuso a primera vista).

Este parece un caso de uso poco probable: no estoy seguro de que debamos optimizar el diseño para él.

las únicas API de BCL en las que puedo pensar donde tenemos tipos anidados

TimeZoneInfo.AdjustmentRule y TimeZoneInfo.TransitionTime son ejemplos en el BCL que se agregaron intencionalmente como tipos anidados.

@justinvp

Creo que necesitamos tener HashCode en el nombre de la API de punto de entrada porque su propósito previsto es ayudar a implementar GetHashCode (), no GetHash ().

👍 Ya veo.

He pensado un poco más en las cosas. Parece razonable tener una estructura anidada; como mencionaste, la mayoría de la gente nunca verá el tipo real. Solo una cosa: creo que el tipo debería llamarse Seed , en lugar de HashCodeValue . El contexto de su nombre ya está implícito en la clase contenedora.

API propuesta

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 ¿ Alguna objeción o problema de implementación con tener public readonly int Value lugar? El problema con Seed es que técnicamente no es una semilla después de la primera cosechadora.

También de acuerdo con @justinvp , Hash debería reservarse para tratar con hashes. Esto se introdujo para simplificar el manejo de HashCode lugar.

@redknightlois Para ser claros, estábamos hablando del nombre de la estructura, no del nombre de la propiedad.

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

El problema con Seed es que técnicamente no es una semilla después de la primera cosechadora.

Es una semilla para la próxima cosechadora, que produce una nueva semilla.

¿Alguna objeción o problema de implementación con tener un valor int de solo lectura público en su lugar?

¿Por qué? int Value { get; } es más idiomático y se puede insertar fácilmente.

Es una semilla para la próxima cosechadora, que produce una nueva semilla.

¿No sería eso una plántula? ;)

@jamesqo En mi experiencia, cuando está rodeado de código complejo, las propiedades tienden a generar un código peor que los campos (entre ellos, no en línea). Además, un campo de solo lectura de un solo int en una estructura se traduce directamente en un registro y, finalmente, cuando el JIT usa solo lectura para la optimización (que no pudo encontrar ningún uso de él todavía con respecto a la generación de código); hay optimizaciones que podrían permitirse porque puede razonar que es de solo lectura. Desde el punto de vista del uso, realmente no existe un único getter.

EDITAR: Además, también impulsa la idea de que esas estructuras son realmente inmutables.

En mi experiencia, cuando está rodeado de propiedades de código complejas, tienden a generar un código peor que los campos (entre ellos, no en línea).

Si encuentra una única compilación que no es de depuración en la que una propiedad implementada automáticamente no siempre está insertada, entonces se trata de un problema de JIT y definitivamente debería solucionarse.

Además, un campo de solo lectura de un solo int en una estructura se traduce directamente en un registro

hay optimizaciones que podrían permitirse porque puede razonar que es de solo lectura.

El campo de respaldo de esta estructura será de solo lectura; la API será un acceso.

No creo que el uso de una propiedad afecte el rendimiento de ninguna manera aquí.

@jamesqo Lo tendré en cuenta cuando los encuentre. Para el código sensible al rendimiento, simplemente ya no uso propiedades debido a eso (memoria muscular en este punto).

¿Puede considerar llamar a la estructura anidada "Estado" en lugar de "Semilla"?

@ellismg Seguro, gracias por la sugerencia. Estaba luchando por encontrar un buen nombre para la estructura interna.

@karelz Creo que esta API finalmente está lista para

@jamesqo @JonHanna ¿por qué necesitamos Combine<T>(T obj) lugar de Combine(object o) ?

¿Por qué necesitamos el Combine?(T obj) en lugar de Combinar (objeto o)?

Este último asignaría si la instancia fuera una estructura.

duh, gracias por la aclaración.

No nos gusta el tipo anidado porque parece complicar el diseño. La raíz del problema fue que no podemos nombrar las estáticas y las no estáticas de la misma manera. Tenemos dos opciones: deshacerse de las estáticas o cambiar el nombre. Creemos que cambiar el nombre a Create tiene más sentido, ya que crea un código bastante legible, en comparación con el constructor predeterminado.

A menos que haya una fuerte oposición, ese es el diseño en el que nos hemos decidido:

`` C #
sistema de espacio de nombres
{
estructura pública HashCode: IEquatable
{
Public static HashCode Create (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();
}

}
''

Esperemos un par de días para recibir comentarios adicionales para averiguar si hay comentarios sólidos sobre la propuesta aprobada. Entonces podemos hacerlo "en juego".

¿Por qué complica el diseño? Podría entender cómo sería malo si realmente tuviéramos que usar el HashCode.State en el código (por ejemplo, para definir el tipo de una variable) pero ¿esperamos que ese sea el caso a menudo? La mayoría de las veces terminaré devolviendo el valor directamente o convirtiéndolo en un int y almacenándolo.

Creo que la combinación de Crear y Combinar es peor.

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

@terrajobst

Creemos que cambiar el nombre a Create tiene más sentido, ya que crea un código bastante legible, en comparación con el constructor predeterminado.

A menos que haya una fuerte oposición, ese es el diseño en el que nos hemos decidido:

Te escuché, pero tuve un pensamiento de último momento mientras trabajaba en la implementación ... ¿Podríamos simplemente agregar una propiedad Zero / Empty estática a HashCode , y luego la gente llama Combine desde allí? Eso nos liberaría de tener que tener 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);
}

¿Alguien más piensa que esto es una buena idea? (Enviaré un PR mientras tanto, y si la gente así lo cree, lo cambiaré en el PR).

@jamesqo , me gusta la idea de Vacío / Cero.

Estaría bien con eso (sin una fuerte preferencia entre Empty vs Create factory) ... @weshaggard @bartonjs @stephentoub @terrajobst, ¿qué piensan ustedes?

Personalmente creo que Create () es mejor; pero me gusta más HashCode.Empty que new HashCode() .

Dado que permite una versión que no tiene operador nuevo, y no impide decidir más adelante que realmente queremos Create como bootstrapper ... :: shrug ::.

Ese es el alcance total de mi retroceso (también conocido como no mucho).

FWIW Yo votaría por Create lugar de Empty / Zero . Prefiero comenzar con un valor real que colgar todo de Empty / Zero . Simplemente se siente / se ve raro.

También desalienta a las personas que siembran con cero, que tiende a ser una semilla pobre.

Prefiero Crear en lugar de Vaciar. Concuerda con lo que pienso al respecto: quiero crear un código hash y mezclar valores adicionales. Yo también estaría bien con el enfoque anidado.

Si bien iba a decir que llamarlo Empty no era una buena idea (y eso ya se ha dicho), después de un tercer pensamiento, sigo pensando que no es una mala solución. ¿Qué tal algo como Builder? Si bien aún es posible usar cero, la palabra lo desalienta un poco a usarlo de inmediato.

@JonHanna solo para aclarar: lo Create , ¿verdad?

Y en un cuarto pensamiento, ¿qué tal con With en lugar de Create?

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

Ejemplo de uso basado en la última discusión (con Create posiblemente reemplazado con un nombre alternativo):

`` c #
public override 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 Conduciría a un código inconsistente + más jitting Creo que debido a combinaciones más genéricas. Siempre podemos revisar esto en otro número si resulta ser deseable.

Por lo que vale, prefiero la versión propuesta originalmente, al menos en uso (no estoy seguro de los comentarios sobre el tamaño del código, jitting, etc.). Parece exagerado tener una estructura adicional y más de 10 miembros diferentes para algo que podría expresarse como un método con algunas sobrecargas de diferente aridad. Tampoco soy fanático de las API de estilo fluido en general, así que quizás eso esté coloreando mi opinión.

No iba a mencionar esto porque es un poco inusual y todavía no estoy seguro de cómo me siento al respecto, pero aquí hay otra idea, solo para asegurarme de que se hayan considerado todas las alternativas ...

¿Qué pasaría si hiciéramos algo similar al HashCodeCombiner "constructor" mutable de ASP.NET Core, con métodos Add similares, pero también incluyéramos soporte para la sintaxis del inicializador de la colección?

Uso:

`` c #
public override int GetHashCode () =>
nuevo 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();
    }
}

Tendría que implementar IEnumerable como mínimo junto con al menos un método Add para habilitar la sintaxis del inicializador de la colección. IEnumerable podría implementarse explícitamente para ocultarlo de intellisense y GetEnumerator podría arrojar NotSupportedException o devolver el valor del código hash como un solo elemento combinado en el enumerable, si alguien le sucediera úselo (lo cual sería raro).

@justinvp , tienes una idea interesante. Sin embargo, respetuosamente no estoy de acuerdo; Creo que HashCode debería mantenerse inmutable para evitar errores con estructuras mutables. También tener que implementar IEnumerable para esto parece un poco artificial / escamoso; si alguien tiene una directiva using System.Linq en el archivo, entonces Cast<> y OfType<> aparecerán como métodos de extensión si ponen un punto junto a HashCode . Creo que deberíamos ceñirnos más a la propuesta actual.

@jamesqo , estoy de acuerdo, de ahí mi vacilación en siquiera mencionarlo. Lo único que me gusta de él es que el uso puede ser más limpio que el encadenamiento, pero eso en sí mismo es otro inconveniente, ya que no está claro que los inicializadores de colección se puedan usar incluso sin ver el uso de muestra.

@MadsTorgersen , @jaredpar , por qué el inicializador de la colección requiere la implementación de IEnumerable \El tercer comentario de @ justinvp arriba.

@jamesqo , estoy de acuerdo en que es mejor mantener esto inmutable (y no IEnumerable \

@mellinoe Creo que eso haría el caso simple un poco más simple, pero también haría cualquier cosa más complicada (y menos clara sobre qué es lo correcto).

Eso incluye:

  1. más elementos de los que tiene sobrecargas
  2. condiciones
  3. bucles
  4. usando comparador

Considere el código de ASP.NET publicado anteriormente sobre este tema (actualizado a la propuesta actual):

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

si (ViewLocationExpanderValues! = null)
{
foreach (elemento var en ViewLocationExpanderValues)
{
hashCode = hashCode
.Combine (elemento.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;

Incluso si ignora llamar a GetHashCode() para comparadores personalizados, encuentro que tener que pasar el valor anterior de hashCode como primer parámetro no es sencillo.

@KrzysztofCwalina De acuerdo con la nota de @ericlippert en The C # Programming Language 1 , se debe a que los inicializadores de colección están (como era de esperar) destinados a ser un azúcar sintáctico para la creación de colecciones, no para la aritmética (que era el otro uso común del método llamado Add ).

1 Debido al funcionamiento de Google Libros, es posible que ese vínculo no funcione para todos.

@KrzysztofCwalina , y tenga en cuenta que requiere IEnumerable no genéricos, no IEnumerable<T> .

@svick , liendre menor en su primer ejemplo anterior: la primera llamada a .Combine sería .Create con la propuesta actual. A menos que usemos el enfoque anidado.

@svick

También haría cualquier cosa más complicada (y menos clara sobre qué es lo correcto)

No sé, el segundo ejemplo es apenas diferente del primero en general, y no es más complejo en mi opinión. Con el segundo enfoque / original, simplemente pasa un montón de códigos hash (creo que el primer parámetro en realidad debería ser IsMainPage.GetHashCode() ), por lo que me parece sencillo. Pero parece que estoy en minoría aquí, así que no presionaré por el enfoque original. No tengo una opinión fuerte; Ambos ejemplos me parecen bastante razonables.

@justinvp Gracias, actualizado. (Fui con la primera propuesta en la primera publicación y no me di cuenta de que estaba desactualizada, probablemente alguien debería actualizarla).

@mellinoe, el problema es en realidad que el segundo puede generar errores sutiles. Este es el código real de uno de nuestros proyectos.

        [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);            
        }

Vivimos con eso, pero estamos lidiando con cosas de muy bajo nivel todos los días; así que no es el desarrollador promedio, eso es seguro. Sin embargo, aquí no es lo mismo combinar v con w que w con v ... lo mismo entre las combinaciones v y w. Las combinaciones de hash no son conmutativas, por lo que encadenar una tras otra puede eliminar un conjunto completo de errores a nivel de API.

Fui con la primera propuesta en la primera publicación y no me di cuenta de que estaba desactualizada, probablemente alguien debería actualizarla.

Hecho.
Por cierto: Esta propuesta es muy difícil de seguir, especialmente los votos ... tantas variaciones (lo que supongo que es bueno ;-))

@karelz Si agregamos Create API, creo que aún podemos agregar Empty . No tiene por qué ser uno u otro, como dijo @bartonjs . Propuesto

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

También desalienta a las personas que siembran con cero, que tiende a ser una semilla pobre.

El algoritmo de hash que estamos eligiendo será el mismo que se usa en HashHelpers hoy, lo que tiene el efecto de que hash(0, x) == x . HashCode.Empty.Combine(x) producirá exactamente los mismos resultados que HashCode.Create(x) , por lo que objetivamente no hay diferencia.

@jamesqo olvidó incluir los Zero adicionales en su última propuesta. Si fue una omisión, ¿puede actualizarlo? Luego, podemos pedirle a la gente que vote por su última propuesta. Parece que las otras alternativas (vea la publicación superior que actualicé) no reciben mucho seguimiento ...

@karelz Gracias por detectarlo, arreglado.

@KrzysztofCwalina para comprobar que te refieres a "Agregar" en el sentido de agregar a una colección, no en otro sentido. No sé si me gusta esta restricción, pero eso es lo que decidimos en ese momento.

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

¿Debería llamarse el parámetro hashCode lugar de hash ya que el valor pasado será un código hash que probablemente se obtenga al llamar a GetHashCode() ?

Empty / Zero

Si terminamos quedándonos con esto, otro nombre a considerar es Default .

@justinvp

¿El parámetro debería llamarse hashCode en lugar de hash, ya que el valor pasado será un código hash que probablemente se obtenga al llamar a GetHashCode ()?

Quería nombrar los parámetros int hash y los parámetros hashCode HashCode hashCode . Sin embargo, pensándolo bien, creo que hashCode sería mejor porque, como mencionaste, hash es un poco vago. Actualizaré la API.

Si terminamos manteniendo esto, otro nombre a considerar es Predeterminado.

Cuando escucho Default pienso "la forma habitual de hacer algo cuando no sabes qué opción elegir", no "el valor predeterminado de una estructura". Por ejemplo, algo como Encoding.Default tiene una connotación completamente diferente.

El algoritmo de hash que estamos eligiendo será el mismo que se usa hoy en HashHelpers, que tiene el efecto de que hash (0, x) == x. HashCode.Empty.Combine (x) producirá exactamente los mismos resultados que HashCode.Create (x), por lo que objetivamente no hay diferencia.

Como alguien que no sabe mucho sobre los aspectos internos de esto, realmente me gusta la simplicidad de HashCode.Create(x).Combine(...) . Create es muy obvio, porque se usa en muchos otros lugares.

Si Empty / Zero / Default no proporciona ningún uso algorítmico, no debería estar allí, en mi opinión.

PD: hilo muy interesante !! ¡Buen trabajo! 👍

@ cwe1ss

Si Empty / Zero / Default no proporciona ningún uso algorítmico, no debería estar allí, en mi opinión.

Tener un campo Empty proporciona un uso algorítmico. Representa un "valor inicial" a partir del cual puede combinar hashes. Por ejemplo, si desea combinar una matriz de hash utilizando estrictamente Create , es bastante 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;
}

Si tienes Empty , se vuelve mucho más 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 es bastante análogo a ImmutableArray<T> para mí. Una matriz vacía no es muy útil por sí misma, pero es muy útil como "punto de partida" para otras operaciones, y es por eso que tenemos una propiedad Empty para ella. Creo que también tendría sentido tener uno por HashCode ; nos quedamos con Create .

@jamesqo He notado que silenciosamente / por accidente cambió el nombre de arg obj a value en su propuesta https://github.com/dotnet/corefx/issues/8034#issuecomment -262661653. Lo cambié de nuevo a obj que en mi opinión captura mejor lo que obtienes. El nombre value está más asociado con el valor hash "int" en este contexto.
Estoy abierto a más discusiones sobre el nombre de arg si es necesario, pero cambiémoslo a propósito y realicemos un seguimiento de la diferencia con la última propuesta aprobada.

He actualizado la propuesta en la parte superior. También llamé diff contra la última versión aprobada de la propuesta.

El algoritmo de hash que estamos eligiendo será el mismo que se usa en HashHelpers hoy

¿Por qué es un buen algoritmo elegir como el que debería usarse en todas partes? ¿Qué suposición va a hacer sobre la combinación de los códigos hash? Si se usa en todas partes, ¿abrirá nuevas vías para los ataques DDoS? (Tenga en cuenta que esto nos ha quemado por el hash de cadenas en el pasado).

¿Qué pasaría si hiciéramos algo similar al "constructor" HashCodeCombiner mutable de ASP.NET Core?

Creo que este es el patrón correcto para usar. Un buen combinador de código hash universal generalmente puede usar más estados del que cabe en el propio código hash, pero luego el patrón fluido se rompe porque pasar estructuras más grandes es un problema de rendimiento.

¿Por qué es un buen algoritmo elegir como el que debería usarse en todas partes?

No debería usarse en todas partes. Vea mi comentario en https://github.com/dotnet/corefx/issues/8034#issuecomment -260790829; está dirigido principalmente a personas que no saben mucho sobre hash. Las personas que saben lo que están haciendo pueden evaluarlo para ver si se ajusta a sus necesidades.

¿Qué suposición va a hacer sobre la combinación de los códigos hash? Si se usa en todas partes, ¿abrirá nuevas vías para los ataques DDoS?

Un problema con el hash actual que tenemos es que hash(0, x) == x . Entonces, si se alimenta una serie de nulos o ceros al hash, seguirá siendo 0. Consulte el código . Esto no quiere decir que los nulos no cuenten, pero ninguno de los nulos iniciales sí lo hace. Estoy considerando usar algo más robusto (pero un poco más caro) como aquí , que agrega una constante mágica para evitar el mapeo de cero a cero.

Creo que este es el patrón correcto para usar. Un buen combinador de código hash universal generalmente puede usar más estados del que cabe en el propio código hash, pero luego el patrón fluido se rompe porque pasar estructuras más grandes es un problema de rendimiento.

No creo que deba haber un combinador universal con un tamaño de estructura grande que intente adaptarse a cada caso de uso. En su lugar, estaba visualizando tipos de códigos hash separados que son todos de tamaño int ( FnvHashCode , etc.) y todos tienen sus propios métodos Combine . Además, estos tipos de "constructores" se mantendrán en el mismo método de todos modos, no se pasarán.

No creo que deba haber un combinador universal con un tamaño de estructura grande que intente adaptarse a cada caso de uso.

¿ASP.NET Core podrá reemplazar su propio combinador de código hash , que tiene un estado de 64 bits actualmente, con este?

Estaba imaginando tipos de códigos hash separados que son todos de tamaño int (FnvHashCode, etc.)

¿No conduce esto a una explosión combinatoria? Debería ser parte de la propuesta de API dejar claro a qué conduce este diseño de API.

@jkotas Presenté objeciones similares al comienzo de la discusión. Tratar con funciones hash requiere conocimiento de la materia. Pero entiendo y apoyo la solución del problema causado en 2001 con la introducción de códigos hash en la raíz misma del marco y no prescribo una receta para combinar hash. Este diseño tiene como objetivo resolver eso para el 99% de los casos (donde no hay conocimiento de la materia disponible o incluso necesario, debido a que las propiedades estadísticas del hash son lo suficientemente buenas). ASP.Net Core debería poder usar incluir dichos combinadores en un marco de propósito general en un ensamblado que no es del sistema como el propuesto para discusión aquí: https://github.com/dotnet/corefx/issues/13757

Estoy de acuerdo en que es una buena idea tener un combinador de código hash que sea obvio para usar en el 99% de los casos. Sin embargo, debe permitir más estados internos que solo 32 bits.

Por cierto: ASP.NET usó el patrón fluido para la combinación de hashcode originalmente, pero dejó de hacerlo porque conduce a errores fáciles de pasar por alto: https://github.com/aspnet/Razor/pull/537

@jkotas con respecto a la seguridad de inundación de hash.
DESCARGO DE RESPONSABILIDAD: No es un experto (debe consultar a uno y MS tiene más de unos pocos sobre el tema) .

He estado mirando a mi alrededor y, si bien no existe un consenso general sobre el tema, hay un argumento que está ganando terreno en la actualidad. Los códigos hash tienen un tamaño de 32 bits, los publiqué antes de un gráfico que muestra la probabilidad de colisiones dado el tamaño del conjunto. Eso significa que no importa qué tan bueno sea su algoritmo (mirando SipHash, por ejemplo), es bastante viable generar muchos hashes y encontrar colisiones en un tiempo razonable (hablando de menos de una hora). Esos problemas deben abordarse en la estructura de datos que contiene los hash, no se pueden resolver en el nivel de la función hash. Pagar rendimiento adicional en dispositivos no criptográficos para protegerse contra la inundación de hash sin reparar la estructura de datos subyacente no resolverá el problema.

EDITAR: Publicaste mientras escribía. A la luz de esto, ¿qué beneficios declara 64bits para usted?

@jkotas Investigué el problema al que te

Reacción a aspnet / Common # 40

Descripción de https://github.com/aspnet/Common/issues/40 :

Detecta el error:

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. Ese argumento es como decir que string debería ser mutable ya que la gente no se da cuenta de que Substring devuelve una nueva cadena. Las estructuras mutables son mucho peores en términos de trampas; Creo que deberíamos mantener la estructura inmutable.

con respecto a la seguridad de inundación de hash.

Hay dos lados de esto: diseño correcto por construcción (estructuras de datos robustas, etc.); y mitigación de los problemas en el diseño existente. Ambos son importantes.

@karelz Respecto a la denominación de parámetros

Me di cuenta de que silenciosamente / por accidente cambió el nombre de arg obj a valor en su propuesta dotnet / corefx # 8034 (comentario). Lo cambié de nuevo a obj, que en mi opinión captura mejor lo que obtienes. El valor del nombre está más asociado con el valor hash "int" en sí mismo en este contexto.
Estoy abierto a más discusiones sobre el nombre de arg si es necesario, pero cambiémoslo a propósito y realicemos un seguimiento de la diferencia con la última propuesta aprobada.

Estoy considerando, en una propuesta futura, agregar API para combinar valores a granel. Por ejemplo: CombineRange(ReadOnlySpan<T>) . Si nombramos esto obj , tendríamos que nombrar el parámetro allí objs , lo que suena muy extraño. Entonces deberíamos nombrarlo item lugar; en el futuro, podemos nombrar el parámetro de intervalo items . Actualizó la propuesta.

@jkotas está de acuerdo, pero el punto aquí es que no estamos mitigando nada a nivel de combinador ...

Lo único que podemos hacer es tener una semilla aleatoria, que para todos los estados y propósitos recuerdo haber visto el código en string y es fijo por compilación. (podría estar equivocado en eso, porque eso fue hace mucho tiempo). Tener una implementación adecuada de semillas aleatorias es la única mitigación que podría aplicarse aquí.

Esto es un desafío, dame tu mejor cadena o función hash de memoria con una semilla aleatoria fija y construiré un conjunto de códigos hash de 32 bits que generarán solo colisiones. No tengo miedo de lanzar un desafío así porque es bastante fácil de hacer, la teoría de la probabilidad está de mi lado. Incluso iría y haría una apuesta, pero sé que ganaré, por lo que esencialmente ya no es una apuesta.

Además ... un análisis más profundo muestra que incluso si la mitigación es la capacidad de tener esas "semillas aleatorias" incorporadas por ejecución, no se necesita un combinador más complicado. Porque esencialmente mitigaste el problema en la fuente.

Digamos que tiene M1 y M2 con diferentes semillas aleatorias rs1 y rs2 ....
M1 emitirá h1 = hash('a', rs1) y h2=hash('b', rs1)
M2 emitirá h1' = hash('a', rs2) y h2'=hash('b', rs2)
El punto clave aquí es que h1 y h1' diferirán con una probabilidad 1/ (int.MaxInt-1) (si hash es lo suficientemente bueno, eso es) que para todos los propósitos es como bueno como se va a poner.
Por lo tanto, cualquier c(x,y) que decida usar (si es lo suficientemente bueno) ya está teniendo en cuenta la mitigación incorporada en la fuente.

EDITAR: Encontré el código, estás usando Marvin32 que cambia en cada dominio ahora. Entonces, la mitigación para cadenas es usar semillas aleatorias por ejecución. Lo cual, como dije, es una mitigación suficientemente buena.

@jkotas

¿ASP.NET Core podrá reemplazar su propio combinador de código hash, que tiene un estado de 64 bits actualmente, con este?

Absolutamente; utiliza el mismo algoritmo hash. Acabo de hacer esta aplicación de prueba para medir el número de colisiones y la ejecuté 10 veces. No hay diferencia significativa con el uso de 64 bits.

Estaba imaginando tipos de códigos hash separados que son todos de tamaño int (FnvHashCode, etc.)

¿No conduce esto a una explosión combinatoria? Debería ser parte de la propuesta de API dejar claro a qué conduce este diseño de API.

@jkotas , no lo hará. El diseño de esta clase no establecerá el diseño de futuras API de hash en piedra. Esos deberían considerarse escenarios más avanzados, deberían ir en una propuesta diferente como dotnet / corefx # 13757, y tendrán una discusión de diseño diferente. Creo que es mucho más importante tener una API simple para un algoritmo hash general, para los novatos que están luchando con anular GetHashCode .

Estoy de acuerdo en que es una buena idea tener un combinador de código hash que sea obvio para usar en el 99% de los casos. Sin embargo, debe permitir más estados internos que solo 32 bits.

¿Cuándo necesitaríamos más estado interno que 32 bits? editar: Si es para permitir que las personas conecten una lógica de hash personalizada, creo (nuevamente) que debería considerarse un escenario avanzado y discutirse en dotnet / corefx # 13757.

está utilizando Marvin32 que cambia en cada dominio ahora

Derecha, la mitigación de la aleatorización del código hash de cadena está habilitada de forma predeterminada en .NET Core. No está habilitado de forma predeterminada para aplicaciones independientes en .NET Framework completo debido a la compatibilidad; solo se habilita a través de peculiaridades (por ejemplo, en entornos de alto riesgo).

Todavía tenemos el código para el hash no aleatorio en .NET Core, pero debería estar bien eliminarlo. No espero que lo necesitemos de nuevo. También haría que el cálculo del código hash de la cadena sea un poco más rápido debido a que ya no se verificará si se debe usar la ruta no aleatoria.

El algoritmo Marvin32 utilizado para calcular los códigos hash de cadenas aleatorias tiene un estado interno de 64 bits. Fue elegido por los expertos en la materia de la EM. Estoy bastante seguro de que tenían una buena razón para usar el estado interno de 64 bits, y no lo han usado solo para hacer las cosas más lentas.

Un combinador de hash de propósito general debe seguir evolucionando esta mitigación: debe usar una semilla aleatoria y un algoritmo de combinación de código de hash lo suficientemente fuerte. Idealmente, usaría el mismo Marvin32 que el hash de cadena aleatorio.

El algoritmo Marvin32 utilizado para calcular los códigos hash de cadenas aleatorias tiene un estado interno de 64 bits. Fue elegido por los expertos en la materia de la EM. Estoy bastante seguro de que tenían una buena razón para usar el estado interno de 64 bits, y no lo han usado solo para hacer las cosas más lentas.

@jkotas , el combinador de código hash al que se vinculó no usa Marvin32. Utiliza el mismo algoritmo ingenuo DJBx33x utilizado por string.GetHashCode no aleatorios.

Un combinador de hash de propósito general debe seguir evolucionando esta mitigación: debe usar una semilla aleatoria y un algoritmo de combinación de código de hash lo suficientemente fuerte. Idealmente, usaría el mismo Marvin32 que el hash de cadena aleatorio.

Este tipo no está diseñado para usarse en lugares propensos a ataques de denegación de servicio de hash. Esto está dirigido a personas que no saben cómo agregar / xor y ayudará a evitar cosas como https://github.com/dotnet/coreclr/pull/4654.

Un combinador de hash de propósito general debe seguir evolucionando esta mitigación: debe usar una semilla aleatoria y un algoritmo de combinación de código de hash lo suficientemente fuerte. Idealmente, usaría el mismo Marvin32 que el hash de cadena aleatorio.

Luego, deberíamos hablar con el equipo de C # para que implementen un algoritmo de hash mitigado ValueTuple . Porque ese código también se utilizará en entornos de alto riesgo. Y, por supuesto, Tuple https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Tuple.cs#L60 o System.Numerics.HashHelpers (utilizado en todo el lugar).

Ahora, antes de que decidamos cómo implementarlo, buscaría los mismos expertos en la materia si vale la pena pagar el costo de un algoritmo de combinación de código hash completamente aleatorio (si existe, por supuesto) aunque no cambiaría la forma en que la API es diseñado bien (según la API propuesta, puede usar un estado de 512 bits y seguir teniendo la misma API pública, si está dispuesto a pagar el costo, por supuesto).

Esto está dirigido a personas que no saben cómo agregar / xor

Precisamente por eso es importante que sea robusto. El valor clave de .NET es que se ocupa de los problemas de las personas que no conocen mejor.

Y mientras estamos en eso, no nos olvidemos de IntPtr https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/IntPtr.cs#L119
Ese es especialmente desagradable, xor es probablemente el peor porque bad chocará con dab .

implementar un algoritmo de hash mitigado ValueTuple

Buen punto. No estoy seguro de si ValueTuple se envió o si todavía es tiempo de hacer esto. Abierto https://github.com/dotnet/corefx/issues/14046.

no nos olvidemos de IntPtr

Estos son errores del pasado ... el listón para corregirlos es mucho más alto.

@jkotas

Estos son errores del pasado ... el listón para corregirlos es mucho más alto.

Pensé que uno de los puntos de .Net Core es que la barra para cambios "pequeños" como ese debería ser mucho más baja. Si alguien depende de la implementación de IntPtr.GetHashCode (que realmente no debería), puede optar por no actualizar su versión de .Net Core.

la barra para cambios "pequeños" como ese debería ser mucho más baja

Sí, lo es, en comparación con .NET Framework completo. Pero aún tiene que hacer el trabajo para lograr que el cambio se lleve a cabo en el sistema y es posible que descubra que no vale la pena el dolor. Un ejemplo reciente es el cambio en el algoritmo hash Tuple<T> que se revirtió debido a que se rompió F #: https://github.com/dotnet/coreclr/pull/6767#issuecomment -256896016

@jkotas

Si tuviéramos que hacer HashCode 64 bits, ¿crees que un diseño inmutable mataría el rendimiento en entornos de 32 bits? Estoy de acuerdo con otros lectores, un patrón de construcción parece ser mucho peor.

Mata al perf - no. Penalización de rendimiento pagada por el azúcar sintáctico: sí.

Penalización de rendimiento pagada por el azúcar sintáctico: sí.

¿Es algo que el JIT podría optimizar en el futuro?

Mata al perf - no.
Penalización de rendimiento pagada por el azúcar sintáctico: sí.

Es más que azúcar sintáctico. Si estuviéramos dispuestos a hacer HashCode una clase, entonces sería azúcar sintáctico. Pero un tipo de valor mutable es una granja de errores.

Citando de antes:

Precisamente por eso es importante que sea robusto. El valor clave de .NET es que se ocupa de los problemas de las personas que no conocen mejor.

Yo diría que un tipo de valor mutable no es una API robusta para la mayoría de las personas que no conocen mejor.

Yo diría que un tipo de valor mutable no es una API robusta para la mayoría de las personas que no conocen mejor.

Estar de acuerdo. Aunque, creo que es lamentable que sea el caso de los tipos de constructores de estructuras mutables. Yo uso de ellos todo el tiempo , debido a que son agradables y apretados. [MustNotCopy] anotaciones ¿alguien?

MustNotCopy es el sueño de un amante de estructuras hecho realidad. @jaredpar?

MustNotCopy es como apilar solo pero aún más difícil de usar 😄

Sugiero no crear ninguna clase, sino crear métodos de extensión 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);
}

¡Eso es todo! Es rápido y fácil de usar.

@AlexRadch No me gusta que eso contamine la lista de métodos para todos los enteros , no solo los que se entienden como hashes.

Además, tiene métodos que continúan una cadena de cálculo del código hash, pero ¿cómo lo inicia? ¿Tienes que hacer algo que no sea obvio, como empezar con cero? Es decir, 0.CombineHash(this.FirstName).CombineHash(this.LastName) .

Actualización: según el comentario en dotnet / corefx # 14046, se decidió que la fórmula hash existente se mantendría por ValueTuple :

@jamesqo Gracias por la ayuda.
Desde la última discusión con @jkotas y @VSadov , estamos bien para seguir adelante con la aleatorización / siembra, pero preferiríamos no adoptar una función hash más cara.
Hacer la aleatorización mantiene la puerta para cambiar la función hash en el futuro si surge la necesidad.

@jkotas , ¿podemos simplemente mantener el hash actual basado en ROL 5 por HashCode y reducirlo a 4 bytes? Esto eliminaría todos los problemas con la copia de estructuras. Podemos hacer que HashCode.Empty representen un valor hash aleatorio.

@svick
Sí, esto contamina los métodos para todos los números enteros, pero se puede colocar en un espacio de nombre separado y si no trabaja con hashes, no lo incluirá y no lo verá.

0.CombineHash(this.FirstName).CombineHash(this.LastName) debe escribirse como this.FirstName.GetHash().CombineHash(this.LastName)

Para implementar a partir de la semilla, puede tener el siguiente método estático

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

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

Entonces, cada clase tendrá una semilla diferente para aleatorizar hashes.

@jkotas , ¿podemos simplemente mantener el hash actual basado en ROL 5 para HashCode y reducirlo a 4 bytes?

Creo que un asistente de construcción de código hash de plataforma pública necesita usar un estado de 64 bits para ser robusto. Si es solo de 32 bits, será propenso a producir malos resultados cuando se utilice para aplicar un hash a más elementos, matrices o colecciones en particular. ¿Cómo se escribe documentación sobre cuándo es una buena idea usarla o no? Sí, son instrucciones extra que se emplean mezclando los bits, pero no creo que importe. Este tipo de instrucciones se ejecutan muy rápido. Mi experiencia es que es mejor mezclar más bits que menos porque los efectos de mezclar muy poco son mucho más severos que hacer demasiado.

Además, todavía tengo preocupaciones sobre la forma propuesta de API. Creo que el problema debe considerarse como la creación de código hash, no como una combinación de código hash. Tal vez sea prematuro agregar esto como API de plataforma, y ​​deberíamos esperar y ver si surge un mejor patrón para esto. Esto no impide que alguien publique un paquete nuget (fuente) con esta API, o que corefx lo use como ayuda interna.

@jkotas tener un estado de 64 bits no garantiza que su salida tenga las propiedades estadísticas adecuadas, la función de combinación en sí debe estar diseñada para usar un estado interno de 64 bits. Además, si la función de combinación es buena (estadísticamente hablando), no existe más que menos mezcla. Si el hash tiene aleatorización, avalancha y otras propiedades estadísticas de interés, la mezcla se tiene en cuenta, ya que técnicamente es una función hash especialmente diseñada.

Vea qué hace una buena función hash (que algunos claramente son como xor : http://softwareengineering.stackexchange.com/questions/49550/which-hashing-algorithm-is-best-for-uniqueness-and -velocidad y https://research.neustar.biz/2012/02/02/choosing-a-good-hash-function-part-3/

@jamesqo Por cierto, me acabo de dar cuenta de que el combinador no funcionará para el caso de: "En realidad estoy combinando hashes (no hashes en tiempo de ejecución) porque la semilla cambiará cada vez". ... constructor público con semilla?

@jkotas

Creo que un asistente de construcción de código hash de plataforma pública necesita usar un estado de 64 bits para ser robusto. Si es solo de 32 bits, será propenso a producir malos resultados cuando se utilice para aplicar un hash a más elementos, matrices o colecciones en particular.

¿Importa esto cuándo terminará condensándose en un solo int al final?

@jamesqo No realmente, el tamaño del estado depende solo de la función, no de la robustez. De hecho, puede empeorar su función hash si la combinación no está diseñada para funcionar de esa manera y, en el mejor de los casos, está desperdiciando recursos porque no puede adquirir la aleatoriedad a partir de la coerción.

Corolario: si está cohesionando, asegúrese de que la función sea estadísticamente excelente o de que esté casi garantizado que la empeorará.

Esto depende de si existe correlación entre los elementos. Si no hay correlación, el estado de 32 bits y el rotl simple (o incluso xor) funcionan bien. Si hay correlación, depende.

Considere si alguien usó esto para construir un código hash de cadena a partir de caracteres individuales. No es que sea probable que alguien haga esto para una cadena, pero demuestra el problema:

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

Daría malos resultados para cadenas con estado de 32 bits y rotl simple debido a que los caracteres en las cadenas del mundo real tienden a estar correlacionados. ¿Con qué frecuencia se correlacionarán los elementos para los que se usa y qué tan malos resultados daría? Es difícil de decir, aunque las cosas en la vida real tienden a correlacionarse de formas inesperadas.

Será genial agregar el siguiente método a la aleatorización de Hash compatible con 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 No lo he probado, así que confío en que lo hizo. Pero eso definitivamente dice algo sobre la función que pretendemos usar. Simplemente no es lo suficientemente bueno, al menos, si desea intercambiar velocidad por confiabilidad (nadie puede hacer cosas estúpidas con él). Por una vez, estoy a favor del diseño de que esta no es una función de hash que no es criptográfica, sino una forma rápida de combinar códigos hash no correlacionados (que son tan aleatorios como es posible).

Si lo que queremos es que nadie haga cosas estúpidas con él, usar un estado de 64 bits no está arreglando nada, solo estamos ocultando el problema. Aún sería posible crear una entrada que aproveche esa correlación. Lo que nos apunta una vez más al mismo argumento que expuse hace 18 días. Ver: https://github.com/dotnet/corefx/issues/8034#issuecomment -261301533

Por una vez, estoy a favor del diseño de que esta no es una función de hash no criptográfica, sino una forma rápida de combinar códigos hash no correlacionados

La forma más rápida de combinar códigos hash no correlacionados es xor ...

Es cierto, pero sabemos que la última vez no funcionó tan bien (IntPtr me viene a la mente). La rotación y XOR (actual) es igual de rápido, sin pérdida si alguien pone algún tipo de material correlacionado.

Agregue la aleatorización del código hash con public static HashCode CreateRandomized(Type type); o con los métodos public static HashCode CreateRandomized<T>(); o con ambos.

@jkotas Creo que he encontrado un patrón mejor para esto. ¿Qué pasa si usamos devoluciones de referencia de C # 7? En lugar de devolver un HashCode cada vez, devolveríamos un ref HashCode que encaja en un 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;
    }
}

El uso sigue siendo el mismo que antes:

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

El único inconveniente es que volvemos a tener una estructura mutable. Pero no creo que haya una manera de no copiar y no tener inmutabilidad al mismo tiempo.

( ref this aún no funciona, pero veo un PR en Roslyn para habilitarlo aquí )


@AlexRadch No creo que sea prudente combinar más el hash con el tipo, ya que obtener el código hash del tipo es caro.

@jamesqo public static HashCode CreateRandomized<T>(); no obtiene el código hash de tipo. Crea HashCode aleatorio para este tipo.

@jamesqo " ref this aún no funciona". Incluso una vez que se solucione el problema de Roslyn, ref this no estará disponible para el repositorio de corefx por un tiempo (no estoy seguro de cuánto tiempo, @stephentoub probablemente pueda establecer expectativas).

La discusión del diseño no converge aquí. Además, los 200 comentarios son muy difíciles de seguir.
Planeamos tomar @jkotas la próxima semana y eliminar la propuesta en la revisión de API el próximo martes. Luego, publicaremos la propuesta aquí para recibir más comentarios.

Por un lado: sugiero cerrar este tema y crear uno nuevo con la "bendita propuesta" cuando la tengamos la semana que viene para aligerar la carga de seguir la larga discusión. Avísame si crees que es una mala idea.

@jcouv Estoy de acuerdo con que aún no funcione, siempre y cuando podamos seguir este diseño cuando se publique. (También creo que es posible solucionar esto temporalmente usando Unsafe .)

@karelz OK: smile:

@karelz me ref this devoluciones para tipos de referencia en lugar de tipos de valor. ref this no se puede devolver de forma segura desde estructuras; vea aquí por qué. Por lo tanto, el compromiso de devolución de referencia no funcionará.

De todos modos, cerraré este tema. Abrí otro problema aquí: https://github.com/dotnet/corefx/issues/14354

Debería poder devolver ref "this" desde una publicación de método de extensión de tipo de valor https://github.com/dotnet/roslyn/pull/15650 aunque supongo que C # vNext ...

@benaadams

Debería poder devolver ref "this" desde un método de extensión de tipo de valor post dotnet / roslyn # 15650 aunque supongo que C # vNext ...

Correcto. Es posible devolver this desde un método de extensión ref this . Sin embargo, no es posible devolver this desde un método de instancia de estructura normal. Hay muchos detalles sangrientos de por vida sobre por qué ese es el caso :(

@redknightlois

si queremos ser estrictos, el único hash debería ser uint , se puede ver como un descuido que el marco devuelva int bajo esa luz.

¿Cumplimiento de CLS? Los enteros sin signo no cumplen con CLS.

¿Fue útil esta página
0 / 5 - 0 calificaciones