If I understand the way the CLR boxes things and treats nullables, as described at Boxing / Unboxing Nullable Types - Why this implementation?, there is still something that confuses me. For example, the following C# 7 code
void C<T>(object o) where T : struct {
if (o is T t)
Console.WriteLine($"Argument is {typeof(T)}: {t}");
}
compiles into the following CIL
IL_0000: ldarg.0
IL_0001: isinst valuetype [mscorlib]System.Nullable`1<!!T>
IL_0006: unbox.any valuetype [mscorlib]System.Nullable`1<!!T>
IL_000b: stloc.1
IL_000c: ldloca.s 1
IL_000e: call instance !0 valuetype [mscorlib]System.Nullable`1<!!T>::GetValueOrDefault()
IL_0013: stloc.0
IL_0014: ldloca.s 1
IL_0016: call instance bool valuetype [mscorlib]System.Nullable`1<!!T>::get_HasValue()
IL_001b: brfalse.s IL_003c
IL_001d: ldstr "Argument is {0}: {1}"
IL_0022: ldtoken !!T
IL_0027: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
IL_002c: ldloc.0
IL_002d: box !!T
IL_0032: call string [mscorlib]System.String::Format(string, object, object)
IL_0037: call void [mscorlib]System.Console::WriteLine(string)
IL_003c: ret
yet the following C#
void D<T>(object o) where T : struct {
if (o is T)
Console.WriteLine($"Argument is {typeof(T)}: {(T) o}");
}
compiles into the following CIL
IL_0000: ldarg.0
IL_0001: isinst !!T
IL_0006: brfalse.s IL_002c
IL_0008: ldstr "Argument is {0}: {1}"
IL_000d: ldtoken !!T
IL_0012: call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
IL_0017: ldarg.0
IL_0018: unbox.any !!T
IL_001d: box !!T
IL_0022: call string [mscorlib]System.String::Format(string, object, object)
IL_0027: call void [mscorlib]System.Console::WriteLine(string)
IL_002c: ret
What I think is happening: Looking at the CIL of the first method, it seems to (1) check if the argument is a [boxed?] Nullable<T>
, pushing it on the stack if it is, and null
otherwise, (2) unboxes it (what if it's null
?), (3) tries to get its value, and default(T)
otherwise, (4) and then check if it has a value or not, branching out if it doesn't. The CIL of the second method is straightforward enough, which simply tries to unbox the argument.
If the semantics of both of the code are equivalent, why does the former case involve unboxing to a Nullable<T>
whereas the former case "just unboxes"? Secondly, in the first CIL, if the object argument were to be a boxed int
, which I currently believe to be exactly what it says on the tin (i.e. a boxed int
rather than a boxed Nullable<int>
), wouldn't the isinst
instruction always fail? Does Nullable<T>
get special treatment even on the CIL level?
Update: After handwriting some MSIL, it seems that object
, if it is indeed a boxed int
, can be unboxed into either an int
or a Nullable<int>
.
.method private static void Foo(object o) cil managed {
.maxstack 1
ldarg.0
isinst int32
brfalse.s L_00
ldarg.0
unbox.any int32
call void [mscorlib]System.Console::WriteLine(int32)
L_00:
ldarg.0
isinst valuetype [mscorlib]System.Nullable`1<int32>
brfalse.s L_01
ldarg.0
unbox valuetype [mscorlib]System.Nullable`1<int32>
call instance !0 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault()
call void [mscorlib]System.Console::WriteLine(int32)
L_01:
ldarg.0
unbox valuetype [mscorlib]System.Nullable`1<int32>
call instance bool valuetype [mscorlib]System.Nullable`1<int32>::get_HasValue()
brtrue.s L_02
ldstr "No value!"
call void [mscorlib]System.Console::WriteLine(string)
L_02:
ret
}