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;
}
}
Respostas:
Contanto que o código hash retornado para nulos seja consistente com o tipo, você deve estar bem. O único requisito para um código hash é que dois objetos considerados iguais compartilhem o mesmo código hash.
Retornar 0 ou -1 para nulo, contanto que você escolha um e o retorne o tempo todo, funcionará. Obviamente, os códigos hash não nulos não devem retornar qualquer valor que você use para nulo.
Perguntas semelhantes:GetHashCode em campos nulos?
O que GetHashCode deve retornar quando o identificador do objeto é nulo?
Os "Comentários" dessa entrada do MSDN apresentam mais detalhes sobre o código hash. Pungente, a documentação não fornece qualquer cobertura ou discussão de valores nulos em tudo - nem mesmo no conteúdo da comunidade.Para resolver seu problema com o enum, reimplemente o código hash para retornar um valor diferente de zero, adicione uma entrada enum "desconhecida" padrão equivalente a nulo ou simplesmente não use enums anuláveis.
Achado interessante, por falar nisso.
Outro problema que vejo com isso geralmente é que o código hash não pode representar um tipo de 4 bytes ou maior que seja anulável sem pelo menos uma colisão (mais conforme o tamanho do tipo aumenta). Por exemplo, o código hash de um int é apenas o int, portanto, ele usa todo o intervalo int. Qual valor nesse intervalo você escolhe para nulo? Qualquer um que você escolher entrará em conflito com o próprio código hash do valor.
As colisões em si não são necessariamente um problema, mas você precisa saber que elas existem. Os códigos hash são usados apenas em algumas circunstâncias. Conforme declarado nos documentos do MSDN, os códigos hash não têm garantia de retornar valores diferentes para objetos diferentes, portanto, não se deve esperar que o façam.
fonte
Object.GetHashCode()
em sua própria classe (ou estrutura), você sabe que esse código só será atingido quando as pessoas realmente tiverem uma instância de sua classe. Essa instância não pode sernull
. É por isso que você não começar a sua substituição deObject.GetHashCode()
comif (this == null) return -1;
Há uma diferença entre "sernull
" e "ser um objeto que possui alguns campos que sãonull
".T
, então(T?)null
e(T?)default(T)
teremos o mesmo código hash (na implementação atual do .NET). Isso poderia ser alterado se os implementadores do .NET alterassem o código hashnull
ou o algoritmo do código hash doSystem.Enum
.HashSet<>
) não funciona alterar o código hash denull
.Lembre-se de que o código hash é usado apenas como uma primeira etapa na determinação da igualdade e [é / deve] nunca (ser) usado como uma determinação de fato sobre se dois objetos são iguais.
Se os códigos hash de dois objetos não forem iguais, eles serão tratados como diferentes (porque assumimos que a implementação subjacente está correta - ou seja, não duvidamos disso). Se eles tiverem o mesmo código hash, então eles devem ser verificados quanto à igualdade real que, no seu caso, o
null
e o valor enum falharão.Como resultado - usar zero é tão bom quanto qualquer outro valor no caso geral.
Claro, haverá situações, como seu enum, em que esse zero é compartilhado com o código hash de um valor real . A questão é se, para você, a minúscula sobrecarga de uma comparação adicional causa problemas.
Em caso afirmativo, defina seu próprio comparador para o caso do anulável para seu tipo particular e certifique-se de que um valor nulo sempre produz um código hash que é sempre o mesmo (é claro!) E um valor que não pode ser gerado pelo subjacente algoritmo de código hash do próprio tipo. Para seus próprios tipos, isso pode ser feito. Para outros - boa sorte :)
fonte
Não precisa ser zero - você poderia aumentar para 42 se quisesse.
Tudo o que importa é a consistência durante a execução do programa.
É apenas a representação mais óbvia, porque
null
geralmente é representado como um zero internamente. O que significa que, durante a depuração, se você vir um código hash igual a zero, ele pode fazer com que você pense: "Hmm .. esse foi um problema de referência nula?"Observe que se você usar um número como
0xDEADBEEF
, então alguém poderia dizer que você está usando um número mágico ... e você estaria. (Você poderia dizer que zero é um número mágico também, e você estaria certo ... exceto que é tão amplamente usado que é uma espécie de exceção à regra.)fonte
Boa pergunta.
Eu apenas tentei codificar isto:
enum Season { Spring, Summer, Autumn, Winter, }
e execute assim:
Season? v = null; Console.WriteLine(v);
retorna
null
se eu fizer, ao invés normal
Season? v = Season.Spring; Console.WriteLine((int)v);
ele retorna
0
, como esperado, ou Spring simples se evitarmos lançar paraint
.Então ... se você fizer o seguinte:
Season? v = Season.Spring; Season? vnull = null; if(vnull == v) // never TRUE
EDITAR
Do MSDN
Se dois objetos forem iguais, o método GetHashCode para cada objeto deve retornar o mesmo valor. No entanto, se dois objetos não forem iguais, os métodos GetHashCode para os dois objetos não precisam retornar valores diferentes
Em outras palavras: se dois objetos têm o mesmo código hash, isso não significa que eles são iguais, porque a igualdade real é determinada por Equals .
De MSDN novamente:
fonte
Pode ter sido qualquer coisa. Eu tendo a concordar que 0 não era necessariamente a melhor escolha, mas provavelmente leva ao menor número de bugs.
Uma função hash absolutamente deve retornar o mesmo hash para o mesmo valor. Uma vez que existe um componente que faz isso, este é realmente o único valor válido para o hash de
null
. Se houvesse uma constante para isso, como, hmobject.HashOfNull
, então alguém implementando umIEqualityComparer
teria que saber como usar esse valor. Se eles não pensarem sobre isso, a chance de usarem 0 é ligeiramente maior do que qualquer outro valor, eu acho.Como mencionei acima, acho que é completamente impossível ponto final, só porque existem tipos que já seguem a convenção de que o hash de nulo é 0.
fonte
EqualityComparer<T>.GetHashCode(T)
para algum tipo particularT
que permitenull
, deve-se fazer algo quando o argumento énull
. Você poderia (1) lançar umArgumentNullException
, (2) retornar0
ou (3) retornar outra coisa. Aceito sua resposta por uma recomendação para sempre voltar0
nessa situação?É 0 por uma questão de simplicidade. Não existe esse requisito rígido. Você só precisa garantir os requisitos gerais de codificação de hash.
Por exemplo, você precisa se certificar de que, se dois objetos forem iguais, seus hashcodes também devem ser iguais. Portanto, diferentes hashcodes devem sempre representar objetos diferentes (mas não é necessariamente verdade vice-versa: dois objetos diferentes podem ter o mesmo hashcode, mesmo que isso aconteça com frequência, então esta não é uma função hash de boa qualidade - ela não tem um boa resistência à colisão).
Claro, eu restrinja minha resposta a requisitos de natureza matemática. Existem também condições técnicas específicas do .NET, que você pode ler aqui . 0 para um valor nulo não está entre eles.
fonte
Portanto, isso poderia ser evitado usando um
Unknown
valor enum (embora pareça um pouco estranhoSeason
a ser desconhecido). Portanto, algo como isso negaria esse 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
Então você teria valores de código hash exclusivos para cada temporada.
fonte
Pessoalmente, acho o uso de valores anuláveis um pouco estranho e tento evitá-los sempre que posso. Seu problema é apenas mais um motivo. Às vezes, eles são muito úteis, mas minha regra é não misturar tipos de valor com nulo, se possível, simplesmente porque eles são de dois mundos diferentes. No .NET framework, eles parecem fazer o mesmo - muitos tipos de valor fornecem
TryParse
método que é uma maneira de separar valores de nenhum valor (null
).No seu caso particular, é fácil se livrar do problema porque você lida com seu próprio
Season
tipo.(Season?)null
para mim significa 'a temporada não é especificada', como quando você tem um formulário da web em que alguns campos não são obrigatórios. Em minha opinião, é melhor especificar esse "valor" especial emenum
si do que usar um pouco desajeitadoNullable<T>
. Será mais rápido (sem boxing) mais fácil de ler (Season.NotSpecified
vsnull
) e resolverá seu problema com códigos hash.É claro que para outros tipos, como
int
você não pode expandir o domínio de valor e denominar um dos valores como especial nem sempre é possível. Mas com oint?
código hash a colisão é um problema muito menor, se é que existe.fonte
Nullable<>
estrutura (onde oHasValue
membro será definidotrue
). Tem certeza de que o problema é realmente menor comint?
? Muitas vezes, usamos apenas alguns valores deint
, e então é equivalente a um enum (que pode, em teoria, ter muitos membros).int
faz mais sentido. Claro que as preferências variam.Tuple.Create( (object) null! ).GetHashCode() // 0 Tuple.Create( 0 ).GetHashCode() // 0 Tuple.Create( 1 ).GetHashCode() // 1 Tuple.Create( 2 ).GetHashCode() // 2
fonte