When value types are null

During a recent code review, I came across something that made me say, "wait, that's not right; how does this even compile?"

Let's say I have this C# code:

using System;

struct Foo { }

class Program {
public void Main() {
var foo = new Foo();
if (foo == null) {
Console.WriteLine("Foo is null.");
} else {
Console.WriteLine("Foo is NOT null.");
}
}
}

Does this compile? No, of course not. We're comparing Foo (a value-type) to null, and of course you can't do that. SharpLab tells us

error CS0019: Operator '==' cannot be applied to operands of type 'Foo' and '<null>'

As it very well should. But what if I make a simple change? I'll add a couple of useful methods to Foo:

struct Foo {
public static bool operator ==(Foo lhs, Foo rhs) => true;
public static bool operator !=(Foo lhs, Foo rhs) => false;
}

Now, SharpLab tells us

Compilation completed.

This is the for-illustrative-purposes version of what my coworker had written in the code I was reviewing. But how can this work? All I did was add some comparison operators, which you should define for all your structs, right?

Alright, so what's going on? Here's a hint: what if, instead, I had written this:

using System;

struct Foo { }

class Program {
public void Main() {
var foo = new Foo();
if (new Nullable<Foo>(foo) == null) {
Console.WriteLine("Foo is null.");
} else {
Console.WriteLine("Foo is NOT null.");
}
}
}

No problem here because I can

  1. Construct a Nullable<T> from a value-type T.
  2. Compare a Nullable<T> to null

Of course, in this case, foo will always have a value, because I constructed it with one.

What's this got to do with the fist example? Because I have used ==, the compiler searches for a suitable operator that can handle the operands. It can't compare Foo directly with null, but it does manage to find that there is an == operator for Nullable<T> and null, which just requires that it can compare two Ts using the == operator. So all it has to do is promote value-type Foo to a Nullable<Foo> in order to perform the comparison with null.

However, I don't think I'm alone in experiencing some surprise that the compiler does this conversion for you as a consequence of implementing operator ==.

And since the promoted Nullable<Foo> always has a value, foo == null will never be true. The compiler can optimize that pretty easily. Let's take a look at the generated IL for Main:

.method public hidebysig
instance void Main () cil managed
{
// Method begins at RVA 0x2056
// Code size 11 (0xb)
.maxstack 8

IL_0000: ldstr "Foo is NOT null."
IL_0005: call void [System.Console]System.Console::WriteLine(string)
IL_000a: ret
} // end of method Program::Main

Note the comparison and branch have been completely removed. It unconditionally prints

Foo is NOT null

However, the compiler won't warn you about this, and if you ever write it, it probably wasn't what you meant.