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