Se o código hash de nulo for sempre zero, em .NET

87

Dado que as coleções System.Collections.Generic.HashSet<>aceitam nullcomo um membro do conjunto, pode-se perguntar qual nulldeveria 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 nulle 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 nulldeveria 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 ArgumentNullExceptionse 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 nulla HashSet<>. A menos que HashSet<>faça algo extraordinário ao lidar com um nullitem (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.Springe null vai terminar no mesmo balde. Isso pode ser determinado inspecionando com muito cuidado os membros da matriz privada m_bucketse 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;
  }
}
Jeppe Stig Nielsen
fonte
1
Eu apoio essa - pergunta muito boa.
Sachin Kainth
26
Por que o código hash para nulo não deve ser zero? Uma colisão de hash não é o fim do mundo, você sabe.
Hot Licks
3
Exceto que é uma colisão bem conhecida e bastante comum. Não que seja ruim ou mesmo um grande problema, é facilmente evitável
Chris Pfohl
8
lol por que estou pensando "se o .NET framework pular de uma ponte, você o seguiria?" ...
Adam Houldsworth
3
Só por curiosidade, o que seria uma temporada nula?
SwDevMan81

Respostas:

25

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.

Adam Houldsworth
fonte
Não acho que as perguntas que você vincula sejam completamente semelhantes. Quando você está substituindo 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 ser null. É por isso que você não começar a sua substituição de Object.GetHashCode()com if (this == null) return -1;Há uma diferença entre "ser null" e "ser um objeto que possui alguns campos que são null".
Jeppe Stig Nielsen
Você diz: Obviamente, os códigos hash não nulos não devem retornar o valor que você usa para nulo. Isso seria ideal, eu concordo. E essa é a razão pela qual fiz minha pergunta em primeiro lugar, porque sempre que escrevermos um enum T, então (T?)nulle (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 hash null ou o algoritmo do código hash do System.Enum.
Jeppe Stig Nielsen
Eu concordo que os links eram para campos internos nulos. Você mencionou que é para IEqualityComparer <T>, em sua implementação o código hash ainda é específico para um tipo, então você ainda está na mesma situação, consistência para o tipo. Retornar o mesmo código hash para nulos de qualquer tipo não importa, pois os nulos não têm um tipo.
Adam Houldsworth
1
Nota: Eu atualizei minha pergunta duas vezes. Acontece que (pelo menos com HashSet<>) não funciona alterar o código hash de null.
Jeppe Stig Nielsen
6

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 nulle 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 :)

Andras Zoltan
fonte
5

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 nullgeralmente é 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.)

user541686
fonte
4

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 para int.

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:

O método GetHashCode para um objeto deve retornar consistentemente o mesmo código hash, contanto que não haja nenhuma modificação no estado do objeto que determina o valor de retorno do método Equals do objeto. Observe que isso é verdadeiro apenas para a execução atual de um aplicativo e que um código hash diferente pode ser retornado se o aplicativo for executado novamente.

Tigran
fonte
6
uma colisão, por definição, significa que dois objetos desiguais têm o mesmo código hash. Você demonstrou que os objetos não são iguais. Agora eles têm o mesmo código hash? De acordo com o OP eles fazem, o que significa que é uma colisão. Agora, não é o fim do mundo ter uma colisão, é simplesmente uma colisão mais provável do que se o hash nulo fosse algo diferente de 0, o que prejudica o desempenho.
Servy
1
Então, o que sua resposta realmente diz? Você diz que Season.Spring não é igual a null. Bem, isso não está errado, mas realmente não responde à pergunta de forma alguma agora.
Servy
2
@Servy: a questão diz: é por isso que tenho o mesmo hascode para 2 objetos diferentes ( null e Spring ). Então a resposta é que não existe causa de colisão mesmo tendo o mesmo hashcode, eles não são iguais, aliás.
Tigran
3
"Resposta: por que não?" Bem, o OP respondeu preventivamente à sua pergunta "por que não". É mais provável que cause colisões do que outro número. Ele estava se perguntando se havia um motivo para o 0 ter sido escolhido, e ninguém respondeu até agora.
Servy
1
Esta resposta não contém nada que o OP já não saiba, evidente pela forma como a pergunta foi feita.
Konrad Rudolph
4

Mas há alguma razão pela qual o código hash de nulo deve ser 0?

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, hm object.HashOfNull, então alguém implementando um IEqualityComparerteria 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.

pelo menos para HashSet <>, nem mesmo é possível alterar o hash de nulo

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.

Roman Starkov
fonte
Quando alguém implementa o método EqualityComparer<T>.GetHashCode(T)para algum tipo particular Tque permite null, deve-se fazer algo quando o argumento é null. Você poderia (1) lançar um ArgumentNullException, (2) retornar 0ou (3) retornar outra coisa. Aceito sua resposta por uma recomendação para sempre voltar 0nessa situação?
Jeppe Stig Nielsen
@JeppeStigNielsen Não tenho certeza sobre jogar contra retornar, mas se você decidir retornar, então definitivamente zero.
Roman Starkov de
2

É 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.

Thomas Calc
fonte
1

Portanto, isso poderia ser evitado usando um Unknownvalor enum (embora pareça um pouco estranho Seasona 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.

SwDevMan81
fonte
1
sim, mas isso não responde realmente a questão. Desta forma, de acordo com a questão, null colidirá com Uknown. O que é uma diferença?
Tigran
@Tigran - Esta versão não usa um tipo anulável
SwDevMan81
Entendo, mas a questão é sobre o tipo anulável.
Tigran
Eu tenho uma cena um milhão de vezes no SO que as pessoas oferecem sugestões de melhorias como respostas.
SwDevMan81
1

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 TryParsemé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 Seasontipo.

(Season?)nullpara 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 em enumsi do que usar um pouco desajeitado Nullable<T>. Será mais rápido (sem boxing) mais fácil de ler ( Season.NotSpecifiedvs null) e resolverá seu problema com códigos hash.

É claro que para outros tipos, como intvocê não pode expandir o domínio de valor e denominar um dos valores como especial nem sempre é possível. Mas com o int?código hash a colisão é um problema muito menor, se é que existe.

Maciej
fonte
Quando você diz "boxing", acho que quer dizer "embrulhar", ou seja, colocar um valor de estrutura dentro de uma Nullable<>estrutura (onde o HasValuemembro será definido true). Tem certeza de que o problema é realmente menor com int?? Muitas vezes, usamos apenas alguns valores de int, e então é equivalente a um enum (que pode, em teoria, ter muitos membros).
Jeppe Stig Nielsen
Geralmente, eu diria que enum é escolhido quando há um número limitado de valores conhecidos necessários (2-10). Se o limite for maior ou nenhum, intfaz mais sentido. Claro que as preferências variam.
Maciej
0
Tuple.Create( (object) null! ).GetHashCode() // 0
Tuple.Create( 0 ).GetHashCode() // 0
Tuple.Create( 1 ).GetHashCode() // 1
Tuple.Create( 2 ).GetHashCode() // 2
Denis535
fonte
1
Essa é uma abordagem interessante. Seria útil editar sua resposta para incluir alguma explicação adicional e, especialmente, considerando a natureza da pergunta.
Jeremy Caney