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:
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
:
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:
No problem here because I can
- Construct a
Nullable<T>
from a value-typeT
. - Compare a
Nullable<T>
tonull
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 T
s 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
:
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.