C # está de acuerdo con comparar tipos de valor con nulos

85

Me encontré con esto hoy y no tengo idea de por qué el compilador de C # no arroja un error.

Int32 x = 1;
if (x == null)
{
    Console.WriteLine("What the?");
}

Estoy confundido en cuanto a cómo x podría ser nulo. Especialmente porque esta asignación definitivamente arroja un error del compilador:

Int32 x = null;

¿Es posible que x se vuelva nulo? ¿Microsoft simplemente decidió no poner esta comprobación en el compilador o se perdió por completo?

Actualización: después de jugar con el código para escribir este artículo, de repente el compilador apareció con una advertencia de que la expresión nunca sería verdadera. Ahora estoy realmente perdido. Puse el objeto en una clase y ahora la advertencia desapareció, pero quedó con la pregunta, ¿un tipo de valor puede terminar siendo nulo?

public class Test
{
    public DateTime ADate = DateTime.Now;

    public Test ()
    {
        Test test = new Test();
        if (test.ADate == null)
        {
            Console.WriteLine("What the?");
        }
    }
}
Joshua Belden
fuente
9
Tu también puedes escribir if (1 == 2). No es trabajo del compilador realizar análisis de ruta de código; para eso están las herramientas de análisis estático y las pruebas unitarias.
Aaronaught
Por qué desapareció la advertencia, vea mi respuesta; y no, no puede ser nulo.
Marc Gravell
1
De acuerdo en el (1 == 2), me preguntaba más acerca de la situación (1 == null)
Joshua Belden
Gracias a todos los que respondieron. Todo tiene sentido ahora.
Joshua Belden
Con respecto al problema de advertencia o no advertencia: si la estructura en cuestión es un "tipo simple", como int, el compilador genera advertencias agradables. Para los tipos simples, el ==operador está definido por la especificación del lenguaje C #. Para otras estructuras (no de tipo simple), el compilador se olvida de emitir una advertencia. Consulte Advertencia del compilador incorrecto al comparar la estructura con un valor nulo para obtener más detalles. Para estructuras que no son tipos simples, el ==operador debe estar sobrecargado por un opeartor ==método que sea miembro de la estructura (de lo contrario, no ==se permite).
Jeppe Stig Nielsen

Respuestas:

119

Esto es legal porque la resolución de sobrecarga del operador tiene un mejor operador único para elegir. Hay un operador == que toma dos entradas que aceptan valores NULL. El int local se puede convertir en un int que acepta valores NULL. El literal nulo se puede convertir en un int que acepta valores NULL. Por lo tanto, este es un uso legal del operador == y siempre resultará falso.

Del mismo modo, también le permitimos decir "si (x == 12,6)", que también siempre será falso. El int local es convertible a doble, el literal es convertible a doble y, obviamente, nunca serán iguales.

Eric Lippert
fuente
4
Re su comentario: connect.microsoft.com/VisualStudio/feedback/…
Marc Gravell
5
@James: (Me retracto de mi comentario erróneo anterior, que he eliminado). Los tipos de valor definidos por el usuario que tienen un operador de igualdad definido por el usuario definido también de forma predeterminada tienen un operador de igualdad definido por el usuario elevado generado para ellos. El operador de igualdad definido por el usuario elevado es aplicable por la razón que indica: todos los tipos de valor son convertibles implícitamente a su tipo que acepta valores NULL correspondiente, al igual que el literal nulo. Es no el caso de que un tipo de valor definido por el usuario que carece de un operador de comparación definida por el usuario es comparable a la literal nulo.
Eric Lippert
3
@James: Claro, puedes implementar tu propio operador == y operator! = Que toman estructuras que aceptan valores NULL. Si existen, el compilador los usará en lugar de generarlos automáticamente. (Y, de paso, lamento que la advertencia para el operador elevado sin sentido en operandos que no aceptan valores NULL no produce una advertencia; ese es un error en el compilador que no hemos podido solucionar).
Eric Lippert
2
¡Queremos nuestra advertencia! Nos lo merecemos.
Jeppe Stig Nielsen
3
@JamesDunne: ¿Qué hay de definir un static bool operator == (SomeID a, String b)y etiquetarlo Obsolete? Si el segundo operando es un literal sin tipo null, sería una mejor coincidencia que cualquier forma que requiera el uso de operadores elevados, pero si es un SomeID?que resulta igual null, el operador elevado ganaría.
supercat
17

No es un error, ya que hay una int?conversión ( ); genera una advertencia en el ejemplo dado:

El resultado de la expresión es siempre 'falso' ya que un valor de tipo 'int' nunca es igual a 'nulo' de tipo 'int?'

Si marca el IL, verá que elimina por completo la rama inalcanzable; no existe en una versión de lanzamiento.

Sin embargo, tenga en cuenta que no genera esta advertencia para estructuras personalizadas con operadores de igualdad. Solía ​​hacerlo en 2.0, pero no en el compilador 3.0. El código aún se elimina (por lo que sabe que no se puede acceder al código), pero no se genera ninguna advertencia:

using System;

struct MyValue
{
    private readonly int value;
    public MyValue(int value) { this.value = value; }
    public static bool operator ==(MyValue x, MyValue y) {
        return x.value == y.value;
    }
    public static bool operator !=(MyValue x, MyValue y) {
        return x.value != y.value;
    }
}
class Program
{
    static void Main()
    {
        int i = 1;
        MyValue v = new MyValue(1);
        if (i == null) { Console.WriteLine("a"); } // warning
        if (v == null) { Console.WriteLine("a"); } // no warning
    }
}

Con el IL (para Main): tenga en cuenta que se ha eliminado todo excepto el MyValue(1)(que podría tener efectos secundarios):

.method private hidebysig static void Main() cil managed
{
    .entrypoint
    .maxstack 2
    .locals init (
        [0] int32 i,
        [1] valuetype MyValue v)
    L_0000: ldc.i4.1 
    L_0001: stloc.0 
    L_0002: ldloca.s v
    L_0004: ldc.i4.1 
    L_0005: call instance void MyValue::.ctor(int32)
    L_000a: ret 
}

esto es básicamente:

private static void Main()
{
    MyValue v = new MyValue(1);
}
Marc Gravell
fuente
1
Alguien me informó de esto internamente recientemente. No sé por qué dejamos de producir esa advertencia. Lo ingresamos como un error.
Eric Lippert
5

El hecho de que una comparación nunca pueda ser cierta no significa que sea ilegal. No obstante, no, un tipo de valor puede serlo null.

Adam Robinson
fuente
1
Pero un tipo de valor podría ser igual a null. Considere int?, para cuál es azúcar sintáctico Nullable<Int32>, cuál es un tipo de valor. int?Ciertamente, una variable de tipo podría ser igual a null.
Greg
1
@Greg: Sí, puede ser igual a nulo, asumiendo que el "igual" al que te refieres es el resultado del ==operador. Sin embargo, es importante tener en cuenta que la instancia no es realmente nula.
Adam Robinson
1

Un tipo de valor no puede ser null, aunque podría ser igual a null(considerar Nullable<>). En su caso, la intvariable y nullse convierten Nullable<Int32>y se comparan implícitamente .

Greg
fuente
0

Sospecho que el compilador está optimizando su prueba particular cuando genera el IL, ya que la prueba nunca será falsa.

Nota al margen: ¿Es posible que un Int32 anulable use Int32? x en su lugar.

GrayWizardx
fuente
0

Supongo que esto se debe a que "==" es un azúcar de sintaxis que en realidad representa la llamada al System.Object.Equalsmétodo que acepta el System.Objectparámetro. Nulo según la especificación ECMA es un tipo especial que, por supuesto, se deriva deSystem.Object .

Por eso solo hay una advertencia.

Vitaly
fuente
Esto no es correcto por dos razones. Primero, == no tiene la misma semántica que Object.Equals cuando uno de sus argumentos es un tipo de referencia. En segundo lugar, nulo no es un tipo. Consulte la sección 7.9.6 de la especificación si desea comprender cómo funciona el operador de igualdad de referencia.
Eric Lippert
"El literal nulo (§9.4.4.6) se evalúa como el valor nulo, que se usa para denotar una referencia que no apunta a ningún objeto o matriz, o la ausencia de un valor. El tipo nulo tiene un solo valor, que es el nulo valor. Por lo tanto, una expresión cuyo tipo es el tipo nulo puede evaluar sólo el valor nulo. No hay forma de escribir explícitamente el tipo nulo y, por lo tanto, no hay forma de usarlo en un tipo declarado ". - esta es una cita de ECMA. ¿De qué estás hablando? Además, ¿qué versión de ECMA usas? No veo 7.9.6 en el mío.
Vitaly
0

[EDITADO: convirtió las advertencias en errores e hizo que los operadores fueran explícitos sobre los valores nulos en lugar del hack de cadenas].

Según la inteligente sugerencia de @ supercat en un comentario anterior, las siguientes sobrecargas de operadores le permiten generar un error sobre las comparaciones de su tipo de valor personalizado con nulo.

Al implementar operadores que se comparan con versiones anulables de su tipo, el uso de null en una comparación coincide con la versión anulable del operador, lo que le permite generar el error a través del atributo Obsolete.

Hasta que Microsoft nos devuelva la advertencia del compilador, voy a optar por esta solución, ¡gracias @supercat!

public struct Foo
{
    private readonly int x;
    public Foo(int x)
    {
        this.x = x;
    }

    public override string ToString()
    {
        return string.Format("Foo {{x={0}}}", x);
    }

    public override int GetHashCode()
    {
        return x.GetHashCode();
    }

    public override bool Equals(Object obj)
    {
        return x.Equals(obj);
    }

    public static bool operator ==(Foo a, Foo b)
    {
        return a.x == b.x;
    }

    public static bool operator !=(Foo a, Foo b)
    {
        return a.x != b.x;
    }

    [Obsolete("The result of the expression is always 'false' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator ==(Foo a, Foo? b)
    {
        return false;
    }
    [Obsolete("The result of the expression is always 'true' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator !=(Foo a, Foo? b)
    {
        return true;
    }
    [Obsolete("The result of the expression is always 'false' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator ==(Foo? a, Foo b)
    {
        return false;
    }
    [Obsolete("The result of the expression is always 'true' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator !=(Foo? a, Foo b)
    {
        return true;
    }
}
yoyó
fuente
A menos que me esté perdiendo algo, su enfoque hará que el compilador chille Foo a; Foo? b; ... if (a == b)..., aunque tal comparación debería ser perfectamente legítima. La razón por la que sugerí el "truco de cadena" es que permitiría la comparación anterior pero chirría if (a == null). En lugar de usar string, se podría sustituir cualquier tipo de referencia que no sea Objecto ValueType; si se desea, se podría definir una clase ficticia con un constructor privado que nunca podría ser llamado y autorizarlo ReferenceThatCanOnlyBeNull.
supercat
Estás absolutamente en lo correcto. Debería haber aclarado que mi sugerencia rompe el uso de nullables ... que en el código base en el que estoy trabajando se consideran pecaminosos de todos modos (boxeo no deseado, etc.). ;)
yoyo
0

Creo que la mejor respuesta a por qué el compilador acepta esto es para clases genéricas. Considere la siguiente clase ...

public class NullTester<T>
{
    public bool IsNull(T value)
    {
        return (value == null);
    }
}

Si el compilador no aceptaba comparaciones nullpara tipos de valor, entonces esencialmente rompería esta clase, teniendo una restricción implícita adjunta a su parámetro de tipo (es decir, solo funcionaría con tipos no basados ​​en valores).

Lee.J.Baxter
fuente
0

El compilador le permitirá comparar cualquier estructura que implemente el == a null. Incluso le permite comparar un int con nulo (sin embargo, recibiría una advertencia).

Pero si desmonta el código, verá que la comparación se resuelve cuando se compila el código. Entonces, por ejemplo, este código (donde Foose implementa una estructura ==):

public static void Main()
{
    Console.WriteLine(new Foo() == new Foo());
    Console.WriteLine(new Foo() == null);
    Console.WriteLine(5 == null);
    Console.WriteLine(new Foo() != null);
}

Genera este IL:

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       45 (0x2d)
  .maxstack  2
  .locals init ([0] valuetype test3.Program/Foo V_0)
  IL_0000:  nop
  IL_0001:  ldloca.s   V_0
  IL_0003:  initobj    test3.Program/Foo
  IL_0009:  ldloc.0
  IL_000a:  ldloca.s   V_0
  IL_000c:  initobj    test3.Program/Foo
  IL_0012:  ldloc.0
  IL_0013:  call       bool test3.Program/Foo::op_Equality(valuetype test3.Program/Foo,
                                                           valuetype test3.Program/Foo)
  IL_0018:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_001d:  nop
  IL_001e:  ldc.i4.0
  IL_001f:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_0024:  nop
  IL_0025:  ldc.i4.1
  IL_0026:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_002b:  nop
  IL_002c:  ret
} // end of method Program::Main

Como puedes ver:

Console.WriteLine(new Foo() == new Foo());

Se traduce a:

IL_0013:  call       bool test3.Program/Foo::op_Equality(valuetype test3.Program/Foo,
                                                               valuetype test3.Program/Foo)

Mientras:

Console.WriteLine(new Foo() == null);

Se traduce a falso:

IL_001e:  ldc.i4.0
hardkoded
fuente