The exception raised by calling default(object).ToString()
is called NullReferenceException
for a reason, it's calling a method on a null reference. default(int?)
on the other hand, is not a null reference, because it's not a reference; it is a value type with a value that is equivalent to null.
The big practical point, is that if this was done, then the following would fail:
default(int?).HasValue // should return false, or throw an exception?
It would also screw-up the way we have some ability to mix nullables and non-nullables:
((int?)null).Equals(1) // should return false, or throw an exception?
And the following becomes completely useless:
default(int?).GetValueOrDefault(-1);
We could get rid of HasValue
and force comparison with null, but then what if the equality override of the value-type that is made nullable can return true when compared to null in some cases. That may not be a great idea, but it can be done and the language has to cope.
Let's think back to why nullable types are introduced. The possibility that a reference type can be null, is inherent in the concept of reference types unless effort is taken to enforce non-nullability: Reference types are types that refer to something, and that implies the possibility of one not referring to anything, which we call null.
While a nuisance in many cases, we can make use of this in a variety of cases, such as representing "unknown value", "no valid value" and so on (we can use it for what null means in databases, for example).
At this point, we've given null a meaning in a given context, beyond the simple fact that a given reference doesn't refer to any object.
Since this is useful, we could therefore want to set an int
or DateTime
to null, but we can't because they aren't types that refer to something else, and hence can't be in a state of not referring to anything any more than I as a mammal can lose my feathers.
The nullable types introduced with 2.0 give us a form of value types that can have the semantic null, through a different mechanism than that of reference types. Most of this you could code yourself if it didn't exist, but special boxing and promotion rules allow for more sensible boxing and operator use.
Okay. Now let's consider why NullReferenceExceptions
happen in the first place. Two are inevitable, and one was a design decision in C# (and doesn't apply to all of .NET).
- You try to call a virtual method or property, or access a field on a null reference. This has to fail, because there's no way to look up what override should be called, and no such field.
- You call a non-virtual method or property on a null reference which in turn calls a virtual method or property, or accesses a field. This is obviously a variant on point one, but the design decision we're coming to next has the advantage of guaranteeing this fails at the start, rather than part-way through (which could be confusing and have long-term side-effects).
- You call a non-virtual method or property on a null reference which does not call a virtual method or property, or access a field. There's no inherent reason why this should not be allowed, and some languages allow it, but in C# they decided to use
callvirt
rather than call
to force a NullReferenceException
for the sake of consistency (can't say I agree, but there you go).
None of these cases apply in any way to a nullable value type. It is impossible to put a nullable value type into a condition in which there is no way to know which field or method override to access. The whole concept of NullReferenceException
just doesn't make sense here.
In all, not throwing a NullReferenceException
is consistent with the other types - types through it if and only if a null reference is used.
Note that there is a case where calling on a null nullable-type throws, it does so with GetType()
, because GetType()
is not virtual, and when called on a value-type there is always an implied boxing. This is true of other value types so:
(1).GetType()
is treated as:
((object)1).GetType()
But in the case of nullable types, boxing turns those with a false HasValue
into null, and hence:
default(int?).GetType()
being treated as:
((object)default(int?)).GetType()
which results in GetType()
being called on a null object, and hence throwing.
This incidentally brings us to why not faking NullReferenceType
was the more sensible design decision - people who need that behaviour can always box. If you want it to through then use ((object)myNullableValue).GetString()
so there's no need for the language to treat it as a special case to force the exception.
EDIT
Oh, I forgot to mention the mechanics behind NullReferenceException
.
The test for NullReferenceException
is very cheap, because it mostly just ignores the problem, and then catches the exception from the OS if it happens. In other words, there is no test.
See What is the CLR implementation behind raising/generating a null reference exception? and note how none of that would work with nullable value types.