C # 可以将值类型与 null 进行比较

我今天碰到了这个问题,不知道为什么 C # 编译器没有抛出错误。

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

我对 x 怎么可能是 null 感到困惑,特别是因为这个作业明确地抛出了一个编译器错误:

Int32 x = null;

有没有可能 x 会变成 null,微软只是决定不把这个检查放入编译器,还是完全错过了?

更新: 编写这篇文章的代码出了问题,编译器突然发出警告,表达式永远不会是真的。现在我真的迷路了。我将对象放入一个类中,现在警告消失了,但留下了一个问题,值类型最终是否为 null。

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


public Test ()
{
Test test = new Test();
if (test.ADate == null)
{
Console.WriteLine("What the?");
}
}
}
26297 次浏览

The fact that a comparison can never be true doesn't mean that it's illegal. Nonetheless, no, a value type can ever be null.

No, Int32 x won't ever become null.

If you are comparing an int to null then the comparison operator that takes two int?s is applicable.

"Why a comparison of a value type with null is a warning?" article will help you.

I suspect that your particular test is just being optimized out by the compiler when it generates the IL since the test will never be false.

Side Note: It is possible to have a nullable Int32 use Int32? x instead.

It isn't an error, as there is a (int?) conversion; it does generate a warning in the example given:

The result of the expression is always 'false' since a value of type 'int' is never equal to 'null' of type 'int?'

If you check the IL, you'll see that it completely removes the unreachable branch - it doesn't exist in a release build.

Note however that it doesn't generate this warning for custom structs with equality operators. It used to in 2.0, but not in the 3.0 compiler. The code is still removed (so it knows that the code is unreachable), but no warning is generated:

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
}
}

With the IL (for Main) - note everything except the MyValue(1) (which could have side-effects) has been removed:

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

this is basically:

private static void Main()
{
MyValue v = new MyValue(1);
}

A value type cannot be null, although it could be equal to null (consider Nullable<>). In your case the int variable and null are implicitly cast to Nullable<Int32> and compared.

I guess this is because "==" is a syntax sugar which actually represents call to System.Object.Equals method that accepts System.Object parameter. Null by ECMA specification is a special type which is of course derived from System.Object.

That's why there's only a warning.

This is legal because operator overload resolution has a unique best operator to choose. There is an == operator that takes two nullable ints. The int local is convertible to a nullable int. The null literal is convertible to a nullable int. Therefore this is a legal usage of the == operator, and will always result in false.

Similarly, we also allow you to say "if (x == 12.6)", which will also always be false. The int local is convertible to a double, the literal is convertible to a double, and obviously they will never be equal.

[EDITED: made warnings into errors, and made operators explicit about nullable rather than the string hack.]

As per @supercat's clever suggestion in a comment above, the following operator overloads allow you to generate an error about comparisons of your custom value type to null.

By implementing operators that compare to nullable versions of your type, the use of null in a comparison matches the nullable version of the operator , which lets you generate the error via the Obsolete attribute.

Until Microsoft gives us back our compiler warning I'm going with this workaround, thanks @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;
}
}

I think the best answer as to why the compiler accepts this is for generic classes. Consider the following class...

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

If the compiler didn't accept comparisons against null for value types, then it would essentially break this class, having an implicit constraint attached to its type parameter (i.e. it would only work with non-value-based types).

The compiler will allow you to compare any struct implementing the == to null. It even allows you to compare an int to null (you would get a warning though).

But if you disassemble the code you will see that the comparison is being solved when the code is compiled. So, for instance, this code (where Foo is a struct implementing ==):

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

Generates this 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

As you can see:

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

Is translated to:

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

Whereas:

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

Is translated to false:

IL_001e:  ldc.i4.0

I'm writing because checking compiler output yields a somewhat different picture than the accepted answer, which speaks of operator overload. To see this, consider this variation of the code from the question:

int x = 1;
int? y = null;


if (x == y)
{
System.Console.WriteLine("What the?");
}

As can be seen with https://sharplab.io/ for example, this corresponds to the following lower-level C#:

int num = 1;
Nullable<int> num2 = null;
if ((num == num2.GetValueOrDefault()) & num2.HasValue)
{
Console.WriteLine("What the?");
}

There is no operator overload here, at least no runtime use of a core library operator that takes two nullables. Rather, the nullable y is turned into a plain integer by GetValueOrDefault. In the special case where y is null, the overall result is false because of HasValue.

(The reason why I did not take the exact code from the question is that the comparison expression would be replaced by false during compilation - that is, the compiler intelligence would obfuscate the issue.)