Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

why does n.GetHashCode() work but n.GetType() throws and exception?

Tags:

c#

exception

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.

like image 339
Hex440bx Avatar asked Apr 05 '11 05:04

Hex440bx


1 Answers

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.

like image 107
Jon Skeet Avatar answered Sep 29 '22 05:09

Jon Skeet