Dado que colecciones como System.Collections.Generic.HashSet<>
aceptar null
como miembro del conjunto, uno puede preguntar cuál null
deberí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 , null
y 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 null
deberí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 ArgumentNullException
if 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 null
a un HashSet<>
. A menos que HashSet<>
haga algo extraordinario al tratar con un null
artí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.Spring
y la null
voy a terminar en el mismo cubo. Esto se puede determinar inspeccionando con mucho cuidado los miembros de la matriz privada m_buckets
y 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;
}
}
Respuestas:
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.
fuente
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 sernull
. Es por eso que no comienza su anulación deObject.GetHashCode()
con.if (this == null) return -1;
Hay una diferencia entre "sernull
" y "ser un objeto que posee algunos campos que sonnull
".T
, entonces(T?)null
y(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 hashnull
o el algoritmo del código hash delSystem.Enum
.HashSet<>
) no funciona cambiar el código hash denull
.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
null
y 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 :)
fuente
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
null
menudo 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).fuente
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 aint
.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:
fuente
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, hmobject.HashOfNull
, entonces alguien que implemente unIEqualityComparer
tendrí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.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.
fuente
EqualityComparer<T>.GetHashCode(T)
para algún tipo particularT
que lo permitenull
, uno tiene que hacer algo cuando el argumento esnull
. Puede (1) lanzar unArgumentNullException
, (2) devolver0
o (3) devolver algo más. Tomo tu respuesta por una recomendación para volver siempre0
en esa situación?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.
fuente
Entonces esto podría evitarse usando un
Unknown
valor de enumeración (aunque parece un poco extrañoSeason
que 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.
fuente
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
TryParse
mé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
Season
tipo.(Season?)null
para 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 enenum
sí mismo en lugar de usar un poco torpeNullable<T>
. Será más rápido (sin boxeo) más fácil de leer (Season.NotSpecified
vsnull
) y resolverá su problema con los códigos hash.Por supuesto, para otros tipos, como
int
no se puede expandir el dominio de valor y denominar uno de los valores como especial no siempre es posible. Pero con laint?
colisión del código hash es un problema mucho menor, si es que lo hay.fuente
Nullable<>
estructura (donde elHasValue
miembro se establecerá entrue
). ¿Estás seguro de que el problema es realmente menorint?
? La mayoría de las veces, uno usa solo unos pocos valores deint
, y luego es equivalente a una enumeración (que en teoría puede tener muchos miembros).int
tiene más sentido. Por supuesto, las preferencias varían.Tuple.Create( (object) null! ).GetHashCode() // 0 Tuple.Create( 0 ).GetHashCode() // 0 Tuple.Create( 1 ).GetHashCode() // 1 Tuple.Create( 2 ).GetHashCode() // 2
fuente