Qual é o preferido: Nullable <T> .HasValue ou Nullable <T>! = Null?

437

Eu sempre usei Nullable<>.HasValueporque gostei da semântica. No entanto, recentemente eu estava trabalhando na base de código existente de outra pessoa, onde ela usava Nullable<> != nullexclusivamente.

Existe uma razão para usar um sobre o outro, ou é pura preferência?

  1. int? a;
    if (a.HasValue)
        // ...

vs.

  1. int? b;
    if (b != null)
        // ...
lc.
fonte
9
Eu fiz uma pergunta semelhante ... tenho algumas boas respostas: stackoverflow.com/questions/633286/...
nailitdown
3
Pessoalmente , eu usaria HasValueuma vez que acho que as palavras tendem a ser mais legíveis que os símbolos. Tudo depende de você, e o que se encaixa no seu estilo existente.
Jake Petroules
1
.HasValuefaz mais sentido, pois indica que o tipo é do tipo, em T?vez de um tipo que pode ser anulável, como seqüências de caracteres.
user3791372

Respostas:

476

O compilador substitui comparações nulas por uma chamada para HasValue, portanto, não há diferença real. Faça o que for mais legível / faça mais sentido para você e seus colegas.

Rex M
fonte
86
Eu acrescentaria que "o que for mais consistente / segue um estilo de codificação existente".
Josh Lee
20
Uau. Eu odeio esse açúcar sintático. int? x = nullme dá a ilusão de que uma instância anulável é um tipo de referência. Mas a verdade é que Nullable <T> é um tipo de valor. Parece que eu obter um NullReferenceException fazer: int? x = null; Use(x.HasValue).
KFL 4/04
11
@KFL Se o açúcar sintático o incomodar, use em Nullable<int>vez de int?.
Cole Johnson
24
Nos estágios iniciais da criação de um aplicativo, você pode pensar que é suficiente usar um tipo de valor anulável para armazenar alguns dados, apenas para perceber, depois de um tempo, que você precisa de uma classe adequada para seu objetivo. Depois de escrever o código original para comparar com nulo, você tem a vantagem de não precisar pesquisar / substituir todas as chamadas para HasValue () por uma comparação nula.
Anders
14
É muito bobo reclamar sobre ser capaz de definir um Nullable como nulo ou compará-lo com nulo, já que é chamado Nullable . O problema é que as pessoas estão confundindo "tipo de referência" com "pode ​​ser nulo", mas isso é uma confusão conceitual. O C # futuro terá tipos de referência não anuláveis.
Jim Balter
48

Eu prefiro (a != null)que a sintaxe corresponda aos tipos de referência.

cbp
fonte
11
O que é bastante enganador, é claro, já que nãoNullable<> é um tipo de referência.
Luaan 5/08/15
9
Sim, mas o fato geralmente importa muito pouco no momento em que você está verificando nulamente.
CBP
31
É apenas enganoso para os conceitualmente confusos. O uso de uma sintaxe consistente para dois tipos diferentes não implica que eles sejam do mesmo tipo. O C # possui tipos de referência nulos (todos os tipos de referência são nulos no momento, mas isso será alterado no futuro) e tipos de valores nulos. Usar uma sintaxe consistente para todos os tipos anuláveis ​​faz sentido. De maneira alguma isso implica que tipos de valor anuláveis ​​são tipos de referência ou que tipos de referência anuláveis ​​são tipos de valor.
Jim Balter
Eu prefiro HasValueporque é mais legível do que o #!= null
Konrad Konrad
A consistência da codificação é mais legível se você não misturar estilos diferentes de escrever o mesmo código. Como nem todos os locais têm uma propriedade .HasValue, é necessário usar! = Null para aumentar a consistência. Na minha opinião.
ColacX 24/09/19
21

Eu fiz algumas pesquisas sobre isso usando métodos diferentes para atribuir valores a um int nulo. Aqui está o que aconteceu quando eu fiz várias coisas. Deve esclarecer o que está acontecendo. Lembre-se: Nullable<something>ou a abreviação something?é uma estrutura para a qual o compilador parece estar fazendo muito trabalho para nos deixar usar com nulo como se fosse uma classe.
Como você verá abaixo, SomeNullable == nulle SomeNullable.HasValuesempre retornará um esperado verdadeiro ou falso. Embora não demonstrado abaixo, também SomeNullable == 3é válido (assumindo que SomeNullable é um int?).
Enquanto SomeNullable.Valueisso, obtemos um erro de tempo de execução se atribuímos nullao método, otimização do compilador e negócios de macaco.SomeNullable . Este é, de fato, o único caso em que anuláveis ​​podem nos causar um problema, graças a uma combinação de operadores sobrecarregados, sobrecarregados.object.Equals(obj)

Aqui está uma descrição de algum código que eu executei e qual saída ele produziu nos rótulos:

int? val = null;
lbl_Val.Text = val.ToString(); //Produced an empty string.
lbl_ValVal.Text = val.Value.ToString(); //Produced a runtime error. ("Nullable object must have a value.")
lbl_ValEqNull.Text = (val == null).ToString(); //Produced "True" (without the quotes)
lbl_ValNEqNull.Text = (val != null).ToString(); //Produced "False"
lbl_ValHasVal.Text = val.HasValue.ToString(); //Produced "False"
lbl_NValHasVal.Text = (!(val.HasValue)).ToString(); //Produced "True"
lbl_ValValEqNull.Text = (val.Value == null).ToString(); //Produced a runtime error. ("Nullable object must have a value.")
lbl_ValValNEqNull.Text = (val.Value != null).ToString(); //Produced a runtime error. ("Nullable object must have a value.")

Ok, vamos tentar o próximo método de inicialização:

int? val = new int?();
lbl_Val.Text = val.ToString(); //Produced an empty string.
lbl_ValVal.Text = val.Value.ToString(); //Produced a runtime error. ("Nullable object must have a value.")
lbl_ValEqNull.Text = (val == null).ToString(); //Produced "True" (without the quotes)
lbl_ValNEqNull.Text = (val != null).ToString(); //Produced "False"
lbl_ValHasVal.Text = val.HasValue.ToString(); //Produced "False"
lbl_NValHasVal.Text = (!(val.HasValue)).ToString(); //Produced "True"
lbl_ValValEqNull.Text = (val.Value == null).ToString(); //Produced a runtime error. ("Nullable object must have a value.")
lbl_ValValNEqNull.Text = (val.Value != null).ToString(); //Produced a runtime error. ("Nullable object must have a value.")

Tudo o mesmo de antes. Lembre-se de que a inicialização com int? val = new int?(null);, com nulo passado para o construtor, teria produzido um erro de tempo COMPILE, pois VALUE do objeto anulável NÃO é anulável. É apenas o próprio objeto wrapper que pode ser igual a nulo.

Da mesma forma, receberíamos um erro de tempo de compilação em:

int? val = new int?();
val.Value = null;

para não mencionar que val.Valueé uma propriedade somente leitura de qualquer maneira, o que significa que não podemos nem usar algo como:

val.Value = 3;

mas, novamente, operadores de conversão implícita sobrecarregados polimórficos nos permitem:

val = 3;

Não há necessidade de se preocupar com o que você chama de polissom, desde que funcione corretamente? :)

Perrin Larson
fonte
5
"Lembre-se: Anulável <algo> ou a abreviação de algo? É uma classe." Isto está errado! Anulável <T> é uma estrutura. Ele sobrecarrega o operador Equals e == para retornar true quando comparado a null. O compilador não faz nenhum trabalho sofisticado para essa comparação.
precisa saber é
1
@andrewjs - Você está certo que é uma estrutura (não uma classe), mas está errado ao sobrecarregar o operador ==. Se você digitar o Nullable<X>VisualStudio 2013 e o F12, verá que ele apenas sobrecarrega a conversão de e para Xo Equals(object other)método. No entanto, acho que o operador == usa esse método por padrão, portanto o efeito é o mesmo. Na verdade, eu pretendo atualizar essa resposta há algum tempo, mas sou preguiçoso e / ou ocupado. Este comentário terá que fazer por agora :)
Perrin Larson
Eu verifiquei rapidamente o ildasm e você está certo sobre o compilador fazer alguma mágica; comparar um objeto Nullable <T> para null realmente se traduz em uma chamada para HasValue. Interessante!
andrewjs
3
@andrewjs Na verdade, o compilador trabalha bastante para otimizar anuláveis. Por exemplo, se você atribuir um valor a um tipo anulável, ele não será realmente anulável (por exemplo, int? val = 42; val.GetType() == typeof(int)). Portanto, não apenas é nula uma estrutura que pode ser igual a nula, como também muitas vezes não é nula! : D Da mesma maneira, quando você coloca um valor nulo em caixa, você está encaixotando int, não int?- e quando int?não tem um valor, você obtém, em nullvez de um valor nulo em caixa. Basicamente, significa raramente há qualquer sobrecarga do uso de anulável corretamente :)
Luaan
1
@JimBalter Realmente? Isso é muito interessante. Então, o que o criador de perfil de memória diz sobre um campo anulável em uma classe? Como você declara um tipo de valor que herda de outro tipo de valor em C #? Como você declara seu próprio tipo anulável que se comporta da mesma forma que o tipo anulável do .NET? Desde quando é Nullum tipo no .NET? Você pode apontar para a parte da especificação CLR / C # onde foi dito? Nullables são bem definidos na especificação CLR, seu comportamento não é "implementação de uma abstração" - é um contrato . Mas se o melhor que você pode fazer é ataques ad hominem, divirta-se.
10266 Luaan
13

No VB.Net. NÃO use "IsNot Nothing" quando puder usar ".HasValue". Acabei de solucionar um erro de confiança médio "A operação poderia desestabilizar o tempo de execução", substituindo "IsNot Nothing" por ".HasValue" em um único local. Eu realmente não entendo o porquê, mas algo está acontecendo de maneira diferente no compilador. Eu diria que "! = Null" em C # pode ter o mesmo problema.

Carter Medlin
fonte
8
Eu preferiria HasValuepor causa da legibilidade. IsNot Nothingé realmente uma expressão feia (por causa da dupla negação).
Stefan Steinegger
12
@steffan "Não é nada" não é dupla negação. "Nada" não é negativo, é uma quantidade discreta, mesmo fora do domínio da programação. "Essa quantidade não é nada." é, gramaticalmente, exatamente o mesmo que dizer "Essa quantidade não é zero". e nem é um duplo negativo.
precisa saber é
5
Não é que eu não queira discordar da ausência de verdade aqui, mas vamos lá agora. IsNot Nada é claramente, bem, excessivamente negativo. Por que não escrever algo positivo e claro como o HasValue? Este não é um teste gramatical, é um código, onde o objetivo principal é a clareza.
precisa saber é o seguinte
3
jmbpiano: Eu concordo que não é dupla negação, mas é uma única negação e é quase tão feia e não tão clara quanto uma simples expressão positiva.
Kaveh Hadjari
0

Se você usa linq e deseja manter seu código curto, recomendo sempre usar !=null

E é por isso que:

Vamos imaginar que temos alguma classe Foocom uma variável dupla anulávelSomeDouble

public class Foo
{
    public double? SomeDouble;
    //some other properties
}   

Se em algum lugar do código queremos obter todos os Foo com valores SomeDouble não nulos de uma coleção de Foo (supondo que alguns foos na coleção também possam ser nulos), terminamos com pelo menos três maneiras de escrever nossa função (se use C # 6):

public IEnumerable<Foo> GetNonNullFoosWithSomeDoubleValues(IEnumerable<Foo> foos)
{
     return foos.Where(foo => foo?.SomeDouble != null);
     return foos.Where(foo=>foo?.SomeDouble.HasValue); // compile time error
     return foos.Where(foo=>foo?.SomeDouble.HasValue == true); 
     return foos.Where(foo=>foo != null && foo.SomeDouble.HasValue); //if we don't use C#6
}

E nesse tipo de situação eu recomendo sempre ir para a mais curta

yan yankelevich
fonte
2
Sim, foo?.SomeDouble.HasValueé um erro em tempo de compilação (não um "lance" na minha terminologia) nesse contexto porque seu tipo é bool?, não apenas bool. (O .Wheremétodo deseja a Func<Foo, bool>.) É permitido fazer isso (foo?.SomeDouble).HasValue, é claro, desde que esse tipo seja bool. É para isso que sua primeira linha é "traduzida" internamente pelo compilador C # (pelo menos formalmente).
Jeppe Stig Nielsen
-6

Resposta geral e regra geral: se você tiver uma opção (por exemplo, escrever serializadores personalizados) para processar Nullable em pipeline diferente de object- e usar suas propriedades específicas - faça-o e use propriedades específicas Nullable. Portanto, do ponto de vista do pensamento consistente, HasValuedeve ser preferido. O pensamento consistente pode ajudá-lo a escrever um código melhor, sem gastar muito tempo em detalhes. Por exemplo, o segundo método será muitas vezes mais eficaz (principalmente por causa dos compiladores embutidos e do boxe, mas os números ainda são muito expressivos):

public static bool CheckObjectImpl(object o)
{
    return o != null;
}

public static bool CheckNullableImpl<T>(T? o) where T: struct
{
    return o.HasValue;
}

Teste de benchmark:

BenchmarkDotNet=v0.10.5, OS=Windows 10.0.14393
Processor=Intel Core i5-2500K CPU 3.30GHz (Sandy Bridge), ProcessorCount=4
Frequency=3233539 Hz, Resolution=309.2587 ns, Timer=TSC
  [Host] : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1648.0
  Clr    : Clr 4.0.30319.42000, 64bit RyuJIT-v4.6.1648.0
  Core   : .NET Core 4.6.25009.03, 64bit RyuJIT


        Method |  Job | Runtime |       Mean |     Error |    StdDev |        Min |        Max |     Median | Rank |  Gen 0 | Allocated |
-------------- |----- |-------- |-----------:|----------:|----------:|-----------:|-----------:|-----------:|-----:|-------:|----------:|
   CheckObject |  Clr |     Clr | 80.6416 ns | 1.1983 ns | 1.0622 ns | 79.5528 ns | 83.0417 ns | 80.1797 ns |    3 | 0.0060 |      24 B |
 CheckNullable |  Clr |     Clr |  0.0029 ns | 0.0088 ns | 0.0082 ns |  0.0000 ns |  0.0315 ns |  0.0000 ns |    1 |      - |       0 B |
   CheckObject | Core |    Core | 77.2614 ns | 0.5703 ns | 0.4763 ns | 76.4205 ns | 77.9400 ns | 77.3586 ns |    2 | 0.0060 |      24 B |
 CheckNullable | Core |    Core |  0.0007 ns | 0.0021 ns | 0.0016 ns |  0.0000 ns |  0.0054 ns |  0.0000 ns |    1 |      - |       0 B |

Código de referência:

public class BenchmarkNullableCheck
{
    static int? x = (new Random()).Next();

    public static bool CheckObjectImpl(object o)
    {
        return o != null;
    }

    public static bool CheckNullableImpl<T>(T? o) where T: struct
    {
        return o.HasValue;
    }

    [Benchmark]
    public bool CheckObject()
    {
        return CheckObjectImpl(x);
    }

    [Benchmark]
    public bool CheckNullable()
    {
        return CheckNullableImpl(x);
    }
}

https://github.com/dotnet/BenchmarkDotNet foi usado

PS . As pessoas dizem que o conselho "prefere o HasValue por causa de um pensamento consistente" não é relacionado e inútil. Você pode prever o desempenho disso?

public static bool CheckNullableGenericImpl<T>(T? t) where T: struct
{
    return t != null; // or t.HasValue?
}

PPS As pessoas continuam menos, mas ninguém tenta prever o desempenho de CheckNullableGenericImpl. E há compilador não irá ajudá-lo a substituir !=nullcom HasValue. HasValuedeve ser usado diretamente se você estiver interessado em desempenho.

Roman Pokrovskij
fonte
2
Suas CheckObjectImpl caixas são anuláveis ​​em um object, enquanto CheckNullableImplque não usam boxe. Assim, a comparação é muito imprecisa. Além de não ser tarifa, também é inútil, porque, conforme observado na resposta aceita , o compilador reescreve !=de HasValuequalquer maneira.
GSerg 16/05
2
Os leitores não ignoram a natureza estrutural de Nullable<T>, você faz (encaixotando-o em um object). Quando você aplica != nullcom um valor nulo à esquerda, não ocorre boxe porque o suporte !=para valores nulos funciona no nível do compilador. É diferente quando você oculta o valor anulável do compilador colocando-o primeiro em um object. Nem o CheckObjectImpl(object o)seu benchmark faz sentido em princípio.
GSerg 16/05
3
Meu problema é que me preocupo com a qualidade do conteúdo deste site. O que você postou é enganoso ou errado. Se você estava tentando responder à pergunta do OP, em seguida, sua resposta é plana fora errado, o que é fácil de provar, substituindo a chamada para CheckObjectImplcom o seu corpo dentro CheckObject. No entanto, seus comentários mais recentes revelam que você realmente tinha uma pergunta completamente diferente em mente quando decidiu responder a essa pergunta de 8 anos, o que torna sua resposta enganosa no contexto da pergunta original. Não era sobre o que o OP estava perguntando.
GSerg 16/05
3
Coloque-se no lugar do próximo cara que pesquise no Google what is faster != or HasValue. Ele chega a essa pergunta, navega pela sua resposta, aprecia sua referência e diz: "Nossa, eu nunca vou usar !=porque é claramente muito mais lento!" Essa é uma conclusão muito errada, a qual ele continuará se espalhando. É por isso que acredito que sua resposta é prejudicial - ela responde a uma pergunta errada e, assim, planta uma conclusão errada no leitor desavisado. Considere o que acontece quando você mudar o seu CheckNullableImplpara também ser return o != null;Você vai obter o mesmo resultado benchmark.
GSerg 16/05
8
Estou discutindo com sua resposta. Sua resposta enganosamente parece que mostra a diferença entre !=e HasValuequando, na verdade, mostra a diferença entre object oe T? o. Se você fizer o que sugeri, ou seja, reescrever CheckNullableImplcomo public static bool CheckNullableImpl<T>(T? o) where T: struct { return o != null; }, você terminará com uma referência que mostra claramente que !=é muito mais lenta que !=. Que deve levá-lo a conclusão de que a questão a sua resposta descreve não é sobre !=vs HasValueem tudo.
GSerg 16/05