I think this is a particularly interesting question. I've a take on it that isn't exactly the same as any of the answers, so bear with my coming to it so late.
The first thing to note, is that null
in .NET is not entirely the same as null
in some other contexts, though it can be used to model them.
In SQL for example, we might have 1 = null
and 1 <> null
both return null
, because it's concept of null
is closer to the mathematical, in which null is not equal to anything, including null, but nor is it precisely unequal to anything.
We can use C# null
for this if we want, but that's not quite how the default meaning of null
in .NET works.
For any reference type, null has a specific defined meaning, and possibly additional meanings provided by context.
The specific meaning is "there is no instance of this an object of this type".
Additional meanings in a given context could be "the integer value is in fact null, rather than matching any integer", or "there is no such collection of items, not even an empty one", or "this is the first such item, so there is no earlier one" and so on depending on how we make use of it. (Also, it could mean "oh dear, that really wasn't supposed to be null, better throw an exception").
Now, we might want to make heavy use of those additional meanings, to such an extent that we define cases in which it is meaningful for an object to be equal to null, or at the very least not unequal to null. However, we have the following limitations:
The specific definition that null for a value of a reference type means there is no instance of that reference type will not go away.
The semantics that x == y
will always give the opposite result to x != y
for a given x and y, will not go away.
Therefore:
If we want a type where an instance can be equal to null, it must be a value type.
We still have to have x != null
return the opposite to x == null
.
Now, since .NET 2.0 we've had a built-in that does precisely that: Nullable<T>
(or T?
in C# syntax). It also has a bit of extra support to help it behave well in the face of boxing (which makes it in man ways behave like a reference type, so without that extra support we'd have complications were x == null
would return true but (object)x == null
would return false.
It's also the case, that Nullable<T>
can deal with pretty much any case in which we'd like to make use of this sort of slightly different semantics for null, meaning there isn't really any case left were we're likely to want to define an Equals()
method that returns true when passed null.