Dado que as coleções System.Collections.Generic.HashSet<>
aceitam null
como um membro do conjunto, pode-se perguntar qual null
deveria ser o código hash . Parece que a estrutura 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
Isso pode ser (um pouco) problemático com enums anuláveis. Se definirmos
enum Season
{
Spring,
Summer,
Autumn,
Winter,
}
então, o Nullable<Season>
(também chamado Season?
) pode assumir apenas cinco valores, mas dois deles, a saber null
e Season.Spring
, têm o mesmo código hash.
É tentador escrever um comparador de igualdade "melhor" 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;
}
}
Mas há alguma razão pela qual o código hash de null
deveria ser 0
?
EDITAR / ADICIONAR:
Algumas pessoas parecem pensar que se trata de substituir Object.GetHashCode()
. Realmente não é, na verdade. (Os autores do .NET fizeram uma substituição de GetHashCode()
na Nullable<>
estrutura que é relevante, no entanto.) Uma implementação escrita pelo usuário do sem parâmetros GetHashCode()
nunca pode lidar com a situação onde está o objeto cujo código hash procuramos null
.
Trata-se de implementar o método abstrato EqualityComparer<T>.GetHashCode(T)
ou de outra forma implementar o método de interface IEqualityComparer<T>.GetHashCode(T)
. Agora, ao criar esses links para o MSDN, vejo que diz ali que esses métodos lançam um ArgumentNullException
se seu único argumento for null
. Isso certamente deve ser um erro no MSDN? Nenhuma das próprias implementações do .NET lança exceções. Nesse caso, jogar quebraria efetivamente qualquer tentativa de adicionar null
a HashSet<>
. A menos que HashSet<>
faça algo extraordinário ao lidar com um null
item (terei que testar isso).
NOVA EDIÇÃO / ADIÇÃO:
Agora tentei depurar. Com HashSet<>
, posso confirmar que, com o comparador de igualdade padrão, os valores Season.Spring
e null
vai terminar no mesmo balde. Isso pode ser determinado inspecionando com muito cuidado os membros da matriz privada m_buckets
e m_slots
. Observe que os índices são sempre, por design, compensados por um.
O código que dei acima não corrige isso. Como se constatou, HashSet<>
nunca perguntará ao comparador de igualdade quando o valor é null
. Este é o código-fonte 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;
}
Isso significa que, pelo menos para HashSet<>
, nem mesmo é possível alterar o hash de null
. Em vez disso, uma solução é alterar o hash de todos os outros valores, como este:
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;
}
}