¿Qué se prefiere: Nullable <T> .HasValue o Nullable <T>! = Null?

437

Siempre solía Nullable<>.HasValueporque me gustaba la semántica. Sin embargo, recientemente estaba trabajando en la base de código existente de otra persona donde la usaban Nullable<> != nullexclusivamente.

¿Hay alguna razón para usar uno sobre el otro, o es puramente preferencial?

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

vs.

  1. int? b;
    if (b != null)
        // ...
lc.
fuente
99
Hice una pregunta similar ... obtuve algunas buenas respuestas: stackoverflow.com/questions/633286/…
nailitdown
3
Personalmente , lo usaría HasValueya que creo que las palabras tienden a ser más legibles que los símbolos. Sin embargo, todo depende de ti y de lo que se ajuste a tu estilo actual.
Jake Petroules
1
.HasValuetiene más sentido ya que denota que el tipo es de tipo en T?lugar de un tipo que puede ser anulable, como cadenas de caracteres.
user3791372

Respuestas:

476

El compilador reemplaza las comparaciones nulas con una llamada a HasValue, por lo que no hay una diferencia real. Simplemente haga lo que sea más legible / tenga más sentido para usted y sus colegas.

Rex M
fuente
86
Yo agregaría a eso "lo que sea más consistente / siga un estilo de codificación existente".
Josh Lee
20
Guau. Odio este azúcar sintáctico. int? x = nullme da la ilusión de que una instancia anulable es un tipo de referencia. Pero la verdad es que Nullable <T> es un tipo de valor. Se siente que tendría un NullReferenceException hacer: int? x = null; Use(x.HasValue).
KFL
11
@KFL Si el azúcar sintáctico te molesta, solo úsalo en Nullable<int>lugar de int?.
Cole Johnson
24
En las primeras etapas de la creación de una aplicación, puede pensar que es suficiente usar un tipo de valor anulable para almacenar algunos datos, solo para darse cuenta después de un tiempo que necesita una clase adecuada para su propósito. Después de haber escrito el código original para comparar con nulo, tiene la ventaja de que no necesita buscar / reemplazar cada llamada a HasValue () con una comparación nula.
Anders
15
Es bastante tonto quejarse de poder establecer un Nullable en nulo o compararlo con nulo dado que se llama Nullable . El problema es que las personas combinan "tipo de referencia" con "puede ser nulo", pero eso es una confusión conceptual. El futuro C # tendrá tipos de referencia no anulables.
Jim Balter
48

Prefiero (a != null)que la sintaxis coincida con los tipos de referencia.

cbp
fuente
11
Lo cual es bastante engañoso, por supuesto, ya Nullable<>que no es un tipo de referencia.
Luaan
99
Sí, pero el hecho generalmente importa muy poco en el momento en que está comprobando nulo.
cbp
31
Es solo engañoso para los conceptualmente confundidos. El uso de una sintaxis consistente para dos tipos diferentes no implica que sean del mismo tipo. C # tiene tipos de referencia anulables (todos los tipos de referencia son anulables actualmente, pero eso cambiará en el futuro) y los tipos de valores anulables. El uso de una sintaxis consistente para todos los tipos anulables tiene sentido. De ninguna manera implica que los tipos de valores anulables son tipos de referencia, o que los tipos de referencia anulables son tipos de valores.
Jim Balter
Prefiero HasValueporque es más legible que!= null
Konrad
La consistencia de codificación es más legible si no combina diferentes estilos de escritura del mismo código. Dado que no todos los lugares tienen una propiedad .HasValue, entonces hace que usar! = Null para aumentar la consistencia. En mi opinión
ColacX
21

Investigué un poco sobre esto mediante el uso de diferentes métodos para asignar valores a un int anulable. Esto es lo que sucedió cuando hice varias cosas. Debería aclarar lo que está pasando. Tenga en cuenta: Nullable<something>o la taquigrafía something?es una estructura para la cual el compilador parece estar haciendo mucho trabajo para permitirnos usar con nulo como si fuera una clase.
Como verá a continuación, SomeNullable == nully SomeNullable.HasValuesiempre devolverá un verdadero o falso esperado. Aunque no se muestra a continuación, también SomeNullable == 3es válido (suponiendo que SomeNullable es un int?).
Aunque SomeNullable.Valueno nos lleva a un error de ejecución, si asignamos nulla SomeNullable. De hecho, este es el único caso en el que los valores nulables pueden causarnos un problema, gracias a una combinación de operadores sobrecargados, sobrecargadosobject.Equals(obj) método y optimización del compilador y negocios de monos.

Aquí hay una descripción de algún código que ejecuté y qué salida produjo en las etiquetas:

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, intentemos el siguiente método de inicialización:

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.")

Todo lo mismo que antes. Tenga en cuenta que la inicialización con int? val = new int?(null);, con nulo pasado al constructor, habría producido un error de tiempo de COMPILACIÓN, ya que el VALOR del objeto anulable NO es anulable. Es solo el objeto contenedor lo que puede ser igual a nulo.

Del mismo modo, obtendríamos un error de tiempo de compilación de:

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

sin mencionar que val.Valuees una propiedad de solo lectura de todos modos, lo que significa que ni siquiera podemos usar algo como:

val.Value = 3;

pero de nuevo, los operadores de conversión implícita sobrecargados polimorfos nos permiten hacer:

val = 3;

Sin embargo, no es necesario preocuparse por lo polis que puede hacer, siempre y cuando funcione correctamente. :)

Perrin Larson
fuente
55
"Tenga en cuenta: Nullable <something> o la taquigrafía algo? Es una clase". ¡Esto está mal! Nullable <T> es una estructura. Sobrecarga el operador Equals y == para devolver verdadero en comparación con nulo. El compilador no hace ningún trabajo sofisticado para esta comparación.
andrewjs
1
@andrewjs: tiene razón en que es una estructura (no una clase), pero está equivocado porque sobrecarga el operador ==. Si escribe Nullable<X>VisualStudio 2013 y F12, verá que solo sobrecarga la conversión hacia y desde X, y el Equals(object other)método. Sin embargo, creo que el operador == usa ese método por defecto, por lo que el efecto es el mismo. De hecho, he tenido la intención de actualizar esta respuesta sobre ese hecho desde hace un tiempo, pero soy vago y / u ocupado. Este comentario tendrá que hacerlo por ahora :)
Perrin Larson
Hice una revisión rápida de ildasm y tienes razón sobre el compilador haciendo algo de magia; comparar un objeto Nullable <T> con nulo de hecho se traduce en una llamada a HasValue. ¡Interesante!
andrewjs
3
@andrewjs En realidad, el compilador hace un montón de trabajo para optimizar los valores nulables. Por ejemplo, si asigna un valor a un tipo anulable, en realidad no será anulable (por ejemplo, int? val = 42; val.GetType() == typeof(int)). Entonces, no solo es anulable una estructura que puede ser igual a nula, sino que a menudo tampoco es anulable en absoluto. : D De la misma manera, cuando encajona un valor anulable, está boxeando int, no int?, y cuando int?no tiene un valor, obtiene en nulllugar de un valor anulable en caja. Básicamente significa que rara vez hay gastos generales por usar nullable correctamente :)
Luaan
1
@JimBalter ¿En serio? Eso es muy interesante. Entonces, ¿qué le dice el generador de perfiles de memoria sobre un campo anulable en una clase? ¿Cómo declara un tipo de valor que hereda de otro tipo de valor en C #? ¿Cómo declara su propio tipo anulable que se comporta igual que el tipo anulable de .NET? ¿Desde cuándo es Nullun tipo en .NET? ¿Puede señalar la parte de la especificación CLR / C # donde se dice eso? Los valores nulables están bien definidos en la especificación CLR, su comportamiento no es "implementación de una abstracción", es un contrato . Pero si lo mejor que puedes hacer es atacar ad hominem, diviértete.
Luaan
13

En VB.Net. NO use "IsNot Nothing" cuando pueda usar ".HasValue". Acabo de resolver un error de confianza medio "Operación podría desestabilizar el tiempo de ejecución" reemplazando "IsNot Nothing" por ".HasValue" en un solo lugar. Realmente no entiendo por qué, pero algo está sucediendo de manera diferente en el compilador. Supongo que "! = Null" en C # puede tener el mismo problema.

Carter Medlin
fuente
8
Preferiría HasValuedebido a la legibilidad. IsNot NothingEs realmente una expresión fea (debido a la doble negación).
Stefan Steinegger
12
@steffan "No es nada" no es doble negación. "Nada" no es negativo, es una cantidad discreta, incluso fuera del ámbito de la programación. "Esta cantidad no es nada". es, gramaticalmente, exactamente lo mismo que decir "Esta cantidad no es cero". y tampoco es un doble negativo.
jmbpiano
55
No es que no quiera estar en desacuerdo con la ausencia de verdad aquí, pero vamos ahora. IsNot Nothing es claramente, bueno, demasiado negativo. ¿Por qué no escribir algo positivo y claro como HasValue? Esta no es una prueba de gramática, es codificación, donde el objetivo clave es la claridad.
Randy Gamage
3
jmbpiano: Estoy de acuerdo que no es una doble negación, pero es una negación única y eso es casi tan feo y no tan claro como una simple expresión positiva.
Kaveh Hadjari
0

Si usa linq y quiere mantener su código corto, recomiendo usar siempre !=null

Y esta es la razón:

Imaginemos que tenemos alguna clase Foocon una variable doble anulableSomeDouble

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

Si en algún lugar de nuestro código queremos obtener todos los Foo con valores SomeDouble no nulos de una colección de Foo (suponiendo que algunos foos en la colección también puedan ser nulos), terminamos con al menos tres formas de escribir nuestra función (si 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
}

Y en este tipo de situación recomiendo ir siempre por la más corta

Yan Yankelevich
fuente
2
Sí, foo?.SomeDouble.HasValuees un error en tiempo de compilación (no un "lanzamiento" en mi terminología) en ese contexto porque su tipo es bool?, no solo bool. (El .Wheremétodo quiere a Func<Foo, bool>.) Está permitido hacerlo (foo?.SomeDouble).HasValue, por supuesto, ya que tiene tipo bool. Esto es lo que su primera línea es "traducida" internamente por el compilador de C # (al menos formalmente).
Jeppe Stig Nielsen
-6

Respuesta general y regla general: si tiene una opción (por ejemplo, escribir serializadores personalizados) para procesar Nullable en una canalización diferente que object, y usar sus propiedades específicas, hágalo y use propiedades específicas de Nullable. Por lo tanto, desde un punto de vista de pensamiento consistente, HasValuedebería preferirse. El pensamiento consistente puede ayudarlo a escribir un mejor código sin perder demasiado tiempo en detalles. Por ejemplo, el segundo método será muchas veces más efectivo (principalmente debido a los compiladores en línea y boxeo, pero aún así los números son muy expresivos):

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

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

Prueba de referencia:

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 referencia:

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 fue utilizado

PS . La gente dice que el consejo "prefiere HasValue debido a un pensamiento constante" no está relacionado e inútil. ¿Puedes predecir el rendimiento de esto?

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

PPS Las personas continúan negativas pero nadie intenta predecir el rendimiento de CheckNullableGenericImpl. Y hay compilador no le ayudará a reemplazar !=nullcon HasValue. HasValuedebe usarse directamente si está interesado en el rendimiento.

Roman Pokrovskij
fuente
2
Sus CheckObjectImpl cajas se anulan en un object, mientras CheckNullableImplque no utiliza el boxeo. Por lo tanto, la comparación es muy inquieta. No solo no es una tarifa, sino que también es inútil porque, como se señaló en la respuesta aceptada , el compilador reescribe !=de HasValuetodos modos.
GSerg
2
Los lectores no ignoran la naturaleza de la estructura Nullable<T>, tú sí (al encajonarlo en un object). Cuando se aplica != nullcon un nulable a la izquierda, no se produce el boxeo porque el soporte !=para nulables funciona en el nivel del compilador. Es diferente cuando ocultas el anulable del compilador al encajonarlo primero en un object. Ni CheckObjectImpl(object o)su punto de referencia tienen sentido en principio.
GSerg
3
Mi problema es que me importa la calidad del contenido en este sitio web. Lo que publicaste es engañoso o incorrecto. Si estaba tratando de responder a la pregunta de la OP, entonces su respuesta es incorrecta plana, lo cual es fácil de demostrar mediante la sustitución de la llamada a CheckObjectImplcon su cuerpo en el interior CheckObject. Sin embargo, sus últimos comentarios revelan que, de hecho, tenía una pregunta completamente diferente en mente cuando decidió responder a esta pregunta de 8 años, lo que hace que su respuesta sea engañosa en el contexto de la pregunta original. No era lo que estaba preguntando el OP.
GSerg
3
Ponte en la piel del próximo tipo que busca en Google what is faster != or HasValue. Llega a esta pregunta, examina su respuesta, aprecia su punto de referencia y dice: "¡Dios, nunca lo usaré !=porque claramente es mucho más lento!" Esa es una conclusión muy errónea que luego procederá a difundir. Es por eso que creo que su respuesta es dañina: responde una pregunta incorrecta y, por lo tanto, planta una conclusión incorrecta en el lector desprevenido. Considere lo que sucede cuando cambia de CheckNullableImpla también ser return o != null;Usted obtendrá el mismo resultado referencia.
GSerg
8
Estoy discutiendo con tu respuesta. Su respuesta parece engañosa que muestra la diferencia entre !=y HasValuecuando, de hecho, muestra la diferencia entre object oy T? o. Si haces lo que te sugerí, es decir, reescribir CheckNullableImplcomo public static bool CheckNullableImpl<T>(T? o) where T: struct { return o != null; }, terminarás con un punto de referencia que muestra claramente que !=es mucho más lento que !=. Lo que debería llevarlo a la conclusión de que el problema que describe su respuesta no se trata en absoluto de !=vs. HasValue
GSerg