[C#] The Nullable paradox, part 2
In the previous part, we saw that we can’t use nullable types with generic constraints. I gave the beginning of an explanation, but now we’ll see the full story.
typeof(T) vs T.GetType()
Everybody knows that for any value type T
, the following:
T value;
value.GetType()
is always equal to
typeof(T)
Right?
Wrong ! Let’s try with a nullable type:
int? i = 0;
Console.WriteLine(i.GetType());
// writes "System.Int32"
Console.WriteLine(typeof(int?));
// writes "System.Nullable`1[System.Int32]"
Curious, isn’t it ?
Boxing optimization
This curious behavior is due to a wonderful optimization done by the CLR.
Indeed, here is what happens when a nullable is boxed:
- If
HasValue==true
, boxValue
, return the reference - If
HasValue==false
, don’t box, returnnull
This is much smarted than blindly boxing the nullable (including the bool
field).
Knowing that, imagine that GetType()
returned the same thing as typeof
, it would means that:
int? i = 0;
Console.WriteLine(i.GetType());
// would write "System.Nullable`1[System.Int32]"
object o = i;
Console.WriteLine(o.GetType());
// would write "System.Int32"
And that would be extremely weird !
Instead, we do have:
o.GetType() == i.GetType()
and that’s great !
Other consequences
Because of this optimization, we also have the following surprising behaviors:
int? i = null;
object o = i;
o.GetType(); // <- throws (reference is null)
i.GetType(); // <- throws (would return System.Int32 but it's null)
o.Equals("hi!"); // <- throws (reference is null)
i.Equals("hi!"); // <- doesn't throw
o.GetHashCode(); // <- throws (reference is null)
i.GetHashCode(); // <- doesn't throw
If int?
really was a struct
, none of those lines would throw.
Conclusion
As we saw, a nullable type has both the behavior of a value type and a reference type.
For one moment, let’s imagine that it would match the struct
constraint:
Type ReturnType<T>(T instance) where T : struct
{
// would throw with a nullable type, if HasValue is false
return instance.GetType();
}
In that case, the general assumption which states that value types can’t be null, would be wrong.
And since a nullable really is a value type by nature, it would have been even more wrong to make it match the class
constraint.
This is why you can’t use it with neither class
nor struct
constraints.
Bonus
Here is a riddle for my early readers:
What other type matches neither the
class
nor thestruct
constraints ?
I’ll give you the answer in the next post ;-)