Si el código hash de null siempre es cero, en .NET

87

Dado que colecciones como System.Collections.Generic.HashSet<>aceptar nullcomo miembro del conjunto, uno puede preguntar cuál nulldebería ser el código hash de . Parece que el marco usa 0:

// nullable struct type
int? i = null;
i.GetHashCode();  // gives 0
EqualityComparer<int?>.Default.GetHashCode(i);  // gives 0

// class type
CultureInfo c = null;
EqualityComparer<CultureInfo>.Default.GetHashCode(c);  // gives 0

Esto puede ser (un poco) problemático con enumeraciones que aceptan valores NULL. Si definimos

enum Season
{
  Spring,
  Summer,
  Autumn,
  Winter,
}

entonces el Nullable<Season>(también llamado Season?) puede tomar solo cinco valores, pero dos de ellos, a saber , nully Season.Spring, tienen el mismo código hash.

Es tentador escribir un comparador de igualdad "mejor" como este:

class NewNullEnumEqComp<T> : EqualityComparer<T?> where T : struct
{
  public override bool Equals(T? x, T? y)
  {
    return Default.Equals(x, y);
  }
  public override int GetHashCode(T? x)
  {
    return x.HasValue ? Default.GetHashCode(x) : -1;
  }
}

Pero, ¿hay alguna razón por la que el código hash de nulldebería ser 0?

EDITAR / ADICIONAR:

Algunas personas parecen pensar que se trata de anular Object.GetHashCode(). Realmente no lo es. (Sin embargo, los autores de .NET hicieron una anulación de GetHashCode()en la Nullable<>estructura que es relevante). Una implementación escrita por el usuario de los parámetros GetHashCode()sin parámetros nunca puede manejar la situación en la que se encuentra el objeto cuyo código hash buscamos null.

Se trata de implementar el método abstracto EqualityComparer<T>.GetHashCode(T)o implementar el método de interfaz IEqualityComparer<T>.GetHashCode(T). Ahora, al crear estos enlaces a MSDN, veo que dice que estos métodos arrojan un ArgumentNullExceptionif su único argumento es null. Sin duda, esto debe ser un error en MSDN. Ninguna de las propias implementaciones de .NET arroja excepciones. Lanzar en ese caso rompería efectivamente cualquier intento de agregar nulla un HashSet<>. A menos que HashSet<>haga algo extraordinario al tratar con un nullartículo (tendré que probarlo).

NUEVA EDICIÓN / ADICIÓN:

Ahora intenté depurar. Con HashSet<>, puedo confirmar que con el comparador de igualdad predeterminado, los valores Season.Springy la null voy a terminar en el mismo cubo. Esto se puede determinar inspeccionando con mucho cuidado los miembros de la matriz privada m_bucketsy m_slots. Tenga en cuenta que los índices siempre, por diseño, están compensados ​​por uno.

Sin embargo, el código que di arriba no soluciona este problema. Resulta HashSet<>que nunca le preguntará al comparador de igualdad cuándo es el valor null. Esto es del código fuente de HashSet<>:

    // Workaround Comparers that throw ArgumentNullException for GetHashCode(null).
    private int InternalGetHashCode(T item) {
        if (item == null) { 
            return 0;
        } 
        return m_comparer.GetHashCode(item) & Lower31BitMask; 
    }

Esto significa que, al menos para HashSet<>, ni siquiera es posible cambiar el hash de null. En cambio, una solución es cambiar el hash de todos los demás valores, así:

class NewerNullEnumEqComp<T> : EqualityComparer<T?> where T : struct
{
  public override bool Equals(T? x, T? y)
  {
    return Default.Equals(x, y);
  }
  public override int GetHashCode(T? x)
  {
    return x.HasValue ? 1 + Default.GetHashCode(x) : /* not seen by HashSet: */ 0;
  }
}
Jeppe Stig Nielsen
fuente
1
Apoyo eso - muy buena pregunta.
Sachin Kainth
26
¿Por qué el código hash de null no debería ser cero? Una colisión de hash no es el fin del mundo, ¿sabes?
Hot Licks
3
Excepto que es una colisión bien conocida y bastante común. No es que sea malo o incluso un problema tan importante, simplemente se puede evitar fácilmente
Chris Pfohl
8
lol ¿por qué estoy pensando "si el marco .NET salta de un puente, lo seguirías?" ...
Adam Houldsworth
3
Solo por curiosidad, ¿cuál sería una temporada nula?
SwDevMan81

Respuestas:

25

Siempre que el código hash devuelto para nulos sea coherente para el tipo, debería estar bien. El único requisito para un código hash es que dos objetos que se consideran iguales compartan el mismo código hash.

Devolver 0 o -1 para nulo, siempre que elija uno y lo devuelva todo el tiempo, funcionará. Obviamente, los códigos hash no nulos no deben devolver el valor que use para nulo.

Preguntas similares:

GetHashCode en campos nulos?

¿Qué debería devolver GetHashCode cuando el identificador del objeto es nulo?

Las "Observaciones" de esta entrada de MSDN dan más detalles sobre el código hash. De manera conmovedora, la documentación no proporciona ninguna cobertura o discusión de valores nulos en absoluto , ni siquiera en el contenido de la comunidad.

Para solucionar su problema con la enumeración, vuelva a implementar el código hash para devolver un valor distinto de cero, agregue una entrada de enumeración "desconocida" predeterminada equivalente a nula, o simplemente no use enumeraciones que aceptan valores NULL.

Interesante hallazgo, por cierto.

Otro problema que veo con esto en general es que el código hash no puede representar un tipo de 4 bytes o más grande que sea anulable sin al menos una colisión (más a medida que aumenta el tamaño del tipo). Por ejemplo, el código hash de un int es solo el int, por lo que usa el rango int completo. ¿Qué valor de ese rango elige para nulo? Cualquiera que elija chocará con el código hash del valor.

Las colisiones en sí mismas no son necesariamente un problema, pero debe saber que están ahí. Los códigos hash solo se utilizan en algunas circunstancias. Como se indica en los documentos de MSDN, no se garantiza que los códigos hash devuelvan valores diferentes para objetos diferentes, por lo que no se debe esperar que lo hagan.

Adam Houldsworth
fuente
No creo que las preguntas que enlazas sean completamente similares. Cuando anula Object.GetHashCode()en su propia clase (o estructura), sabe que este código solo se activará cuando las personas realmente tengan una instancia de su clase. Esa instancia no puede ser null. Es por eso que no comienza su anulación de Object.GetHashCode()con. if (this == null) return -1;Hay una diferencia entre "ser null" y "ser un objeto que posee algunos campos que son null".
Jeppe Stig Nielsen
Usted dice: Obviamente, los códigos hash no nulos no deben devolver el valor que use para nulo. Eso sería ideal, estoy de acuerdo. Y esa es la razón por la que hice mi pregunta en primer lugar, porque siempre que escribimos una enumeración T, entonces (T?)nully (T?)default(T)tendremos el mismo código hash (en la implementación actual de .NET). Eso podría cambiarse si los implementadores de .NET cambiaran el código hash null o el algoritmo del código hash del System.Enum.
Jeppe Stig Nielsen
Estoy de acuerdo en que los enlaces eran para campos internos nulos. Menciona que es para IEqualityComparer <T>, en su implementación el código hash sigue siendo específico de un tipo, por lo que todavía se encuentra en la misma situación, consistencia para el tipo. No importará devolver el mismo código hash para nulos de cualquier tipo, ya que los nulos no tienen un tipo.
Adam Houldsworth
1
Nota: Actualicé mi pregunta dos veces. Resulta que (al menos con HashSet<>) no funciona cambiar el código hash de null.
Jeppe Stig Nielsen
6

Tenga en cuenta que el código hash se usa como un primer paso para determinar la igualdad solamente, y [se / debería] nunca (ser) usado como una determinación de facto sobre si dos objetos son iguales.

Si los códigos hash de dos objetos no son iguales, entonces se tratan como no iguales (porque asumimos que la implementación infiel es correcta, es decir, no lo adivinamos). Si tienen el mismo código hash, entonces se debe verificar la igualdad real que, en su caso, el nully el valor de enumeración fallarán.

Como resultado, usar cero es tan bueno como cualquier otro valor en el caso general.

Claro, habrá situaciones, como su enumeración, en las que este cero se comparte con el código hash de un valor real . La pregunta es si, para usted, la minúscula sobrecarga de una comparación adicional causa problemas.

Si es así, defina su propio comparador para el caso del anulable para su tipo particular, y asegúrese de que un valor nulo siempre produzca un código hash que sea siempre el mismo (¡por supuesto!) Y un valor que el subyacente no pueda proporcionar propio algoritmo de código hash del tipo. Para sus propios tipos, esto es factible. Para otros, buena suerte :)

Andras Zoltan
fuente
5

No tiene que ser cero , podrías hacerlo 42 si quisieras.

Lo único que importa es la coherencia durante la ejecución del programa.

Es solo la representación más obvia, porque a nullmenudo se representa internamente como un cero. Lo que significa que, durante la depuración, si ve un código hash de cero, es posible que piense: "Hmm ... ¿fue este un problema de referencia nula?"

Tenga en cuenta que si usa un número como 0xDEADBEEF, entonces alguien podría decir que está usando un número mágico ... y en cierto modo lo estaría. (Se podría decir que el cero también es un número mágico, y tendría razón ... excepto que se usa tan ampliamente que es una excepción a la regla).

usuario541686
fuente
4

Buena pregunta.

Intenté codificar esto:

enum Season
{
  Spring,
  Summer,
  Autumn,
  Winter,
}

y ejecuta esto así:

Season? v = null;
Console.WriteLine(v);

vuelve null

si lo hago, en cambio normal

Season? v = Season.Spring;
Console.WriteLine((int)v);

regresa 0, como se esperaba, o Spring simple si evitamos lanzar a int.

Entonces ... si haces lo siguiente:

Season? v = Season.Spring;  
Season? vnull = null;   
if(vnull == v) // never TRUE

EDITAR

De MSDN

Si dos objetos se comparan como iguales, el método GetHashCode para cada objeto debe devolver el mismo valor. Sin embargo, si dos objetos no se comparan como iguales, los métodos GetHashCode para los dos objetos no tienen que devolver valores diferentes.

En otras palabras: si dos objetos tienen el mismo código hash, eso no significa que sean iguales, porque la igualdad real está determinada por Equals .

De MSDN nuevamente:

El método GetHashCode para un objeto debe devolver constantemente el mismo código hash siempre que no haya ninguna modificación en el estado del objeto que determine el valor de retorno del método Equals del objeto. Tenga en cuenta que esto es cierto solo para la ejecución actual de una aplicación y que se puede devolver un código hash diferente si la aplicación se ejecuta nuevamente.

Tigran
fuente
6
una colisión, por definición, significa que dos objetos desiguales tienen el mismo código hash. Has demostrado que los objetos no son iguales. ¿Ahora tienen el mismo código hash? Según el OP lo hacen, lo que significa que se trata de una colisión. Ahora, no es el fin del mundo tener una colisión, es simplemente una colisión más probable que si el valor hash nulo sea distinto de 0, lo que perjudica el rendimiento.
Servicio
1
Entonces, ¿qué dice realmente tu respuesta? Dices que Season.Spring no es igual a nulo. Bueno, eso no está mal, pero realmente no responde a la pregunta de ninguna manera ahora.
Servicio
2
@Servy: la pregunta dice: por eso tengo el mismo código de acceso para 2 objetos diferentes ( nulo y Spring ). Entonces, la respuesta es que no hay colisión porque incluso teniendo el mismo código hash, no son iguales, por cierto.
Tigran
3
"Respuesta: ¿por qué no?" Bueno, el OP respondió de manera preventiva a su pregunta de "por qué no". Es más probable que cause colisiones que otro número. Se preguntaba si había una razón por la que se eligió 0 y nadie ha respondido hasta ahora.
Servicio
1
Esta respuesta no contiene nada que el OP no sepa ya, evidente por la forma en que se hizo la pregunta.
Konrad Rudolph
4

Pero, ¿hay alguna razón por la que el código hash de null debería ser 0?

Podría haber sido cualquier cosa. Tiendo a estar de acuerdo en que 0 no era necesariamente la mejor opción, pero es una que probablemente conduce a la menor cantidad de errores.

Una función hash debe devolver absolutamente el mismo hash por el mismo valor. Una vez que existe un componente que hace esto, este es realmente el único valor válido para el hash de null. Si hubiera una constante para esto, como, hm object.HashOfNull, entonces alguien que implemente un IEqualityComparertendría que saber cómo usar ese valor. Si no lo piensan, la probabilidad de que usen 0 es ligeramente mayor que cualquier otro valor, supongo.

al menos para HashSet <>, ni siquiera es posible cambiar el hash de null

Como se mencionó anteriormente, creo que es completamente imposible punto final, solo porque existen tipos que ya siguen la convención de que el hash de nulo es 0.

Roman Starkov
fuente
Cuando uno implementa el método EqualityComparer<T>.GetHashCode(T)para algún tipo particular Tque lo permite null, uno tiene que hacer algo cuando el argumento es null. Puede (1) lanzar un ArgumentNullException, (2) devolver 0o (3) devolver algo más. Tomo tu respuesta por una recomendación para volver siempre 0en esa situación?
Jeppe Stig Nielsen
@JeppeStigNielsen No estoy seguro acerca de lanzar vs regresar, pero si eliges regresar, definitivamente cero.
Roman Starkov
2

Es 0 en aras de la simplicidad. No existe un requisito tan estricto. Solo necesita asegurarse de los requisitos generales de la codificación hash.

Por ejemplo, debe asegurarse de que si dos objetos son iguales, sus códigos hash siempre deben ser iguales también. Por lo tanto, diferentes códigos hash siempre deben representar objetos diferentes (pero no es necesariamente cierto al revés: dos objetos diferentes pueden tener el mismo código hash, aunque si esto sucede a menudo, esta no es una función hash de buena calidad, no tiene una buena resistencia a colisiones).

Por supuesto, restringí mi respuesta a requisitos de naturaleza matemática. También existen condiciones técnicas específicas de .NET, que puede leer aquí . 0 para un valor nulo no se encuentra entre ellos.

Thomas Calc
fuente
1

Entonces esto podría evitarse usando un Unknownvalor de enumeración (aunque parece un poco extraño Seasonque no se conozca). Entonces, algo como esto anularía este problema:

public enum Season
{
   Unknown = 0,
   Spring,
   Summer,
   Autumn,
   Winter
}

Season some_season = Season.Unknown;
int code = some_season.GetHashCode(); // 0
some_season = Season.Autumn;
code = some_season.GetHashCode(); // 3

Entonces tendrías valores de código hash únicos para cada temporada.

SwDevMan81
fuente
1
sí, pero esto no responde realmente a la pregunta. De esta manera, de acuerdo con la pregunta, null chocará con Uknown. ¿Qué es una diferencia?
Tigran
@Tigran - Esta versión no utiliza un tipo que
acepta valores
Ya veo, pero la pregunta es sobre el tipo que acepta valores NULL.
Tigran
Tengo una escena un millón de veces en SO que la gente ofrece sugerencias para mejorar como respuestas.
SwDevMan81
1

Personalmente, el uso de valores que aceptan valores NULL me resulta un poco incómodo y trato de evitarlos siempre que puedo. Tu problema es solo otra razón. Sin embargo, a veces son muy útiles, pero mi regla general es no mezclar tipos de valores con nulos si es posible simplemente porque son de dos mundos diferentes. En .NET Framework, parecen hacer lo mismo: muchos tipos de valores proporcionan un TryParsemétodo que es una forma de separar valores de ningún valor ( null).

En su caso particular, es fácil deshacerse del problema porque maneja su propio Seasontipo.

(Season?)nullpara mí significa 'la temporada no está especificada', como cuando tienes un formulario web donde algunos campos no son obligatorios. En mi opinión, es mejor especificar ese 'valor' especial en enumsí mismo en lugar de usar un poco torpe Nullable<T>. Será más rápido (sin boxeo) más fácil de leer ( Season.NotSpecifiedvs null) y resolverá su problema con los códigos hash.

Por supuesto, para otros tipos, como intno se puede expandir el dominio de valor y denominar uno de los valores como especial no siempre es posible. Pero con la int?colisión del código hash es un problema mucho menor, si es que lo hay.

Maciej
fuente
Cuando dices "boxing", creo que te refieres a "envolver", es decir, poner un valor de estructura dentro de una Nullable<>estructura (donde el HasValuemiembro se establecerá en true). ¿Estás seguro de que el problema es realmente menor int?? La mayoría de las veces, uno usa solo unos pocos valores de int, y luego es equivalente a una enumeración (que en teoría puede tener muchos miembros).
Jeppe Stig Nielsen
En general, diría que enum se elige cuando hay un número limitado de valores conocidos requeridos (2-10). Si el límite es mayor o nulo, inttiene más sentido. Por supuesto, las preferencias varían.
Maciej
0
Tuple.Create( (object) null! ).GetHashCode() // 0
Tuple.Create( 0 ).GetHashCode() // 0
Tuple.Create( 1 ).GetHashCode() // 1
Tuple.Create( 2 ).GetHashCode() // 2
Denis535
fuente
1
Ese es un enfoque interesante. Sería útil editar su respuesta para incluir alguna explicación adicional, y especialmente dada la naturaleza de la pregunta.
Jeremy Caney