[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:

  1. If HasValue==true, box Value, return the reference
  2. If HasValue==false, don’t box, return null

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 the struct constraints ?

I’ll give you the answer in the next post ;-)