I am teaching myself C# (I don't know much yet). In this simple example:
bool? n = null;
Console.WriteLine("n = {0}", n);
Console.WriteLine("n.ToString() = {0}", n.ToString());
Console.WriteLine("n.GetHashCode() = {0}", n.GetHashCode());
// this next statement causes a run time exception
Console.WriteLine("n.GetType() = {0}", n.GetType());
Intuitively I understand why the GetType() method would throw an exception. The instance n is null which would explain that but, why don't I get an exception for the same reason when using n.GetHashCode() and ToString() ?
Thank you for your help,
John.
GetHashCode()
is a virtual method overridden in Nullable<T>
: when it's called on a Nullable<T>
value, the Nullable<T>
implementation is used, without any boxing.
GetType()
isn't a virtual method, which means that when it's called, the value is boxed first... and boxing a "null" nullable value results in a null reference - hence the exception. We can see this from the IL:
static void Main()
{
bool? x = null;
Type t = x.GetType();
}
is compiled to:
.method private hidebysig static void Main() cil managed
{
.entrypoint
.maxstack 1
.locals init (
[0] valuetype [mscorlib]System.Nullable`1<bool> nullable,
[1] class [mscorlib]System.Type 'type')
L_0000: nop
L_0001: ldloca.s nullable
L_0003: initobj [mscorlib]System.Nullable`1<bool>
L_0009: ldloc.0
L_000a: box [mscorlib]System.Nullable`1<bool>
L_000f: callvirt instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()
L_0014: stloc.1
L_0015: ret
}
The important bit here is L_000a: the box
instruction before the callvirt
instruction at L_000f.
Now compare that with the equivalent code calling GetHashCode
:
static void Main()
{
bool? x = null;
int hash = x.GetHashCode();
}
compiles to:
.method private hidebysig static void Main() cil managed
{
.entrypoint
.maxstack 1
.locals init (
[0] valuetype [mscorlib]System.Nullable`1<bool> nullable,
[1] int32 num)
L_0000: nop
L_0001: ldloca.s nullable
L_0003: initobj [mscorlib]System.Nullable`1<bool>
L_0009: ldloca.s nullable
L_000b: constrained [mscorlib]System.Nullable`1<bool>
L_0011: callvirt instance int32 [mscorlib]System.Object::GetHashCode()
L_0016: stloc.1
L_0017: ret
}
This time we have a constrained
instruction/prefix before callvirt
, which essentially means "You don't need to box when you call the virtual method." From the OpCodes.Constrained
documentation:
The constrained prefix is designed to allow callvirt instructions to be made in a uniform way independent of whether thisType is a value type or a reference type.
(Follow the link for more information.)
Note that the way boxing of nullable value types work also means that even for a non-null value, you won't get Nullable<T>
. For example consider:
int? x = 10;
Type t = x.GetType();
Console.WriteLine(t == typeof(int?)); // Prints False
Console.WriteLine(t == typeof(int)); // Prints True
So the type you get out is the non-nullable type involved. A call to object.GetType()
will never return a Nullable<T>
type.