I'm delving into C# in Depth, and playing with nullable value types. Just for experimental purposes I wrote a piece of code:
private static void HowNullableWorks()
{
int test = 3;
int? implicitConversion = test;
Nullable<int> test2 = new Nullable<int>(3);
MethodThatTakesNullableInt(null);
MethodThatTakesNullableInt(39);
}
And I was supprised to see that implicitConversion / test2 variables are initialized with:
call instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
instruction, whereas when MethodThatTakesNullableInt is called I can see:
IL_0017: initobj valuetype [mscorlib]System.Nullable`1<int32>
and
IL_0026: newobj instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
which I understand. I thought that I'll see newobj instruction for implicitConversion / test2 as well.
This is full IL code:
.method private hidebysig static void HowNullableWorks() cil managed
{
// Code size 50 (0x32)
.maxstack 2
.locals init ([0] int32 test,
[1] valuetype [mscorlib]System.Nullable`1<int32> implicitConversion,
[2] valuetype [mscorlib]System.Nullable`1<int32> test2,
[3] valuetype [mscorlib]System.Nullable`1<int32> CS$0$0000)
IL_0000: nop
IL_0001: ldc.i4.3
IL_0002: stloc.0
IL_0003: ldloca.s implicitConversion
IL_0005: ldloc.0
IL_0006: call instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
IL_000b: nop
IL_000c: ldloca.s test2
IL_000e: ldc.i4.3
IL_000f: call instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
IL_0014: nop
IL_0015: ldloca.s CS$0$0000
IL_0017: initobj valuetype [mscorlib]System.Nullable`1<int32>
IL_001d: ldloc.3
IL_001e: call void csharp.in.depth._2nd.Program::MethodThatTakesNullableInt(valuetype [mscorlib]System.Nullable`1<int32>)
IL_0023: nop
IL_0024: ldc.i4.s 39
IL_0026: newobj instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
IL_002b: call void csharp.in.depth._2nd.Program::MethodThatTakesNullableInt(valuetype [mscorlib]System.Nullable`1<int32>)
IL_0030: nop
IL_0031: ret
} // end of method Program::HowNullableWorks
First of all, it looks like you've compiled in Debug mode (based on the nop
s) - it's possible that you'll see different code emitted if you compile in Release mode.
Section I.12.1.6.2.1 of the ECMA CLR spec (Initializing instances of value types) says:
There are three options for initializing the home of a value type instance. You can zero it by loading the address of the home (see Table I.8: Address and Type of Home Locations) and using the
initobj
instruction (for local variables this is also accomplished by setting thelocalsinit
bit in the method’s header). You can call a user-defined constructor by loading the address of the home (see Table I.8: Address and Type of Home Locations) and then calling the constructor directly. Or you can copy an existing instance into the home, as described in §I.12.1.6.2.2.
The first three uses of nullable types in your code result in null values stored in locals, so this comment is relevant (locals are one type of home for values): the first two are the locals implicitConversion
and test
that you've declared, and the third is a compiler-generated temporary called CS$0$0000
. As the ECMA spec indicates, these locals can be initialized by using initobj
(which is equivalent to the default no-args constructor for a struct, and is used for CS$0$0000
in this case) or by loading the local's address and calling a constructor (used for the other two locals).
However, for the final nullable instance (created by the implicit conversion from 39
), the result is not stored in a local - it's generated on the stack, so the rules for initializing a home don't apply here. Instead, the compiler just uses newobj
to create the value on the stack (as it would for any value or reference type).
You may be wondering why the compiler generated a local for the call to MethodThatTakesNullableInt(null)
but not for MethodThatTakesNullableInt(39)
. I suspect that the answer is that the compiler always uses initobj
to call the default constructor (which then requires a local or other home for the value), but uses newobj
to call other constructors and store the result on the stack when there's not already an appropriate home for the value.
For more information, see also this comment from Section III.4.21 (newobj) from the spec:
Value types are not usually created using
newobj
. They are usually allocated either as arguments or local variables, usingnewarr
(for zero-based, one-dimensional arrays), or as fields of objects. Once allocated, they are initialized usinginitobj
. However, thenewobj
instruction can be used to create a new instance of a value type on the stack, that can then be passed as an argument, stored in a local, etc.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With