Introduction
static void NullableTest() { int? a = null; object aObj = a; int? b = new int?(); object bObj = b; Console.WriteLine(Object.ReferenceEquals(aObj, bObj)); // True or False? }
1. Assume that int? is a reference type.
2. Assume that int? is a value type.
3. We assume that here we use Nullable<T>.
Investigation
Simple way
Interesting way
int?, Nullable<T>
int? aVal = null; int? bVal = new int?(); Nullable<int> cVal = null; Nullable<int> dVal = new Nullable<int>();
.locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0, valuetype [System.Runtime]System.Nullable`1<int32> V_1, valuetype [System.Runtime]System.Nullable`1<int32> V_2, valuetype [System.Runtime]System.Nullable`1<int32> V_3) // aVal ldloca.s V_0 initobj valuetype [System.Runtime]System.Nullable`1<int32> // bVal ldloca.s V_1 initobj valuetype [System.Runtime]System.Nullable`1<int32> // cVal ldloca.s V_2 initobj valuetype [System.Runtime]System.Nullable`1<int32> // dVal ldloca.s V_3 initobj valuetype [System.Runtime]System.Nullable`1<int32>
- int? is a value type.
- int? is the same as Nullable<int>. The IL code works with Nullable<int32>
- int? aVal = null is the same as Nullable<int> aVal = new Nullable<int>(). In IL, this is compiled to an initobj instruction that performs default initialization by the loaded address.
int? aVal = 62;
.locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0) ldloca.s V_1 ldc.i4.s 62 call instance void valuetype [System.Runtime]System.Nullable`1<int32>::.ctor(!0)
int? aVal = 62; Nullable<int> bVal = new Nullable<int>(62);
// int? aVal; // Nullable<int> bVal; .locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0, valuetype [System.Runtime]System.Nullable`1<int32> V_1) // aVal = 62 ldloca.s V_0 ldc.i4.s 62 call instance void valuetype [System.Runtime]System.Nullable`1<int32>::.ctor(!0) // bVal = new Nullable<int>(62) ldloca.s V_1 ldc.i4.s 62 call instance void valuetype [System.Runtime]System.Nullable`1<int32>::.ctor(!0)
bool IsDefault(int? value) => value == null;
.method private hidebysig instance bool IsDefault(valuetype [System.Runtime]System.Nullable`1<int32> 'value') cil managed { .maxstack 8 ldarga.s 'value' call instance bool valuetype [System.Runtime]System.Nullable`1<int32>::get_HasValue() ldc.i4.0 ceq ret }
bool IsDefaultVerbose(Nullable<int> value) => !value.HasValue;
IL code:
.method private hidebysig instance bool IsDefaultVerbose(valuetype [System.Runtime]System.Nullable`1<int32> 'value') cil managed { .maxstack 8 ldarga.s 'value' call instance bool valuetype [System.Runtime]System.Nullable`1<int32>::get_HasValue() ldc.i4.0 ceq ret }
- Nullable value types are implemented using the Nullable<T> type;
- int? is actually a constructed type of the unbound generic value type Nullable<T>;
- int? a = null is the initialization of an object of Nullable<int> type with the default value, no null is actually present here;
- if (a == null) - again, there is no null, there is a call of the Nullable<T>.HasValue property.
- implicit conversion operator from T to Nullable<T>;
- explicit conversion operator from Nullable<T> to T.
- T value - the value itself, the wrapper over which is Nullable<T>;
- bool hasValue - the flag indicating "whether the wrapper contains a value". It's in quotation marks, since in fact Nullable<T> always contains a value of type T.
Nullable<T> boxing
int aVal = 62; object obj1 = aVal; object obj2 = aVal; Console.WriteLine(Object.ReferenceEquals(obj1, obj2));
Nullable<int> aVal = 62; object obj1 = aVal; object obj2 = aVal; Console.WriteLine(Object.ReferenceEquals(obj1, obj2));
Nullable<int> aVal = new Nullable<int>(); object obj1 = aVal; object obj2 = aVal; Console.WriteLine(Object.ReferenceEquals(obj1, obj2));
Example N1.
C# code:
int aVal = 62; object aObj = aVal;
IL code:
.locals init (int32 V_0, object V_1) // aVal = 62 ldc.i4.s 62 stloc.0 // aVal boxing ldloc.0 box [System.Runtime]System.Int32 // saving the received reference in aObj stloc.1
Example N2.
C# code:
Nullable<int> aVal = 62; object aObj = aVal;
IL code:
.locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0, object V_1) // aVal = new Nullablt<int>(62) ldloca.s V_0 ldc.i4.s 62 call instance void valuetype [System.Runtime]System.Nullable`1<int32>::.ctor(!0) // aVal boxing ldloc.0 box valuetype [System.Runtime]System.Nullable`1<int32> // saving the received reference in aObj stloc.1
Example N3.
C# code:
Nullable<int> aVal = new Nullable<int>(); object aObj = aVal;
IL code:
.locals init (valuetype [System.Runtime]System.Nullable`1<int32> V_0, object V_1) // aVal = new Nullable<int>() ldloca.s V_0 initobj valuetype [System.Runtime]System.Nullable`1<int32> // aVal boxing ldloc.0 box valuetype [System.Runtime]System.Nullable`1<int32> // saving the received reference in aObj stloc.1
- the state of the Nullable<T> object is taken into account (the HasValue flag we discussed earlier is checked). If Nullable<T> does not contain a value (HasValue - false), the result of boxing is null;
- if Nullable<T> contains a value (HasValue - true), it is not a Nullable<T> object that is boxed, but an instance of type T that is stored in the value field of type Nullable<T>;
- specific logic for handling Nullable<T> boxing is not implemented at the C# level or even at the IL level - it is implemented in the CLR.
First:
Nullable<int> aVal = 62; object obj1 = aVal; object obj2 = aVal; Console.WriteLine(Object.ReferenceEquals(obj1, obj2));
- T -> int;
- value -> 62;
- hasValue -> true.
Second:
Nullable<int> aVal = new Nullable<int>(); object obj1 = aVal; object obj2 = aVal; Console.WriteLine(Object.ReferenceEquals(obj1, obj2));
- T -> int;
- value -> default (in this case, 0 - a default value for int);
- hasValue -> false.
static void NullableTest() { int? a = null; // default value of Nullable<int> object aObj = a; // null int? b = new int?(); // default value of Nullable<int> object bObj = b; // null Console.WriteLine(Object.ReferenceEquals(aObj, bObj)); // null == null }
OBJECTREF Nullable::Box(void* srcPtr, MethodTable* nullableMT) { CONTRACTL { THROWS; GC_TRIGGERS; MODE_COOPERATIVE; } CONTRACTL_END; FAULT_NOT_FATAL(); // FIX_NOW: why do we need this? Nullable* src = (Nullable*) srcPtr; _ASSERTE(IsNullableType(nullableMT)); // We better have a concrete instantiation, // or our field offset asserts are not useful _ASSERTE(!nullableMT->ContainsGenericVariables()); if (!*src->HasValueAddr(nullableMT)) return NULL; OBJECTREF obj = 0; GCPROTECT_BEGININTERIOR (src); MethodTable* argMT = nullableMT->GetInstantiation()[0].AsMethodTable(); obj = argMT->Allocate(); CopyValueClass(obj->UnBox(), src->ValueAddr(nullableMT), argMT); GCPROTECT_END (); return obj; }
if (!*src->HasValueAddr(nullableMT)) return NULL;
OBJECTREF obj = 0; GCPROTECT_BEGININTERIOR (src); MethodTable* argMT = nullableMT->GetInstantiation()[0].AsMethodTable(); obj = argMT->Allocate(); CopyValueClass(obj->UnBox(), src->ValueAddr(nullableMT), argMT);
Conclusion
Credits
Article Type : | Guest Article |
Author : | Sergey Vasiliev |
Tags : | CSharp, Knowledge |
Article Date : | 30-10-2020 |
Article Publish Date : | 20-11-2020 |
Note : All content of this article are copyright of their author. |