Reflection and Nullable<T>


You might have already read
this
: VS2005 made the last-minute DCR related to boxed Nullable<T>.
Runtime now treats Nullable<T> differently from other generic value types
when boxing:

Int32? x = null;

object y = x; 


// y is simply null, not a truly boxed “nullable<int> struct” (which then
tells it has no value)

And

Int32 ? x = 100;


object y = x; 


// y is boxed Int32 100 (same as “object y = 100”), no longer
a boxed “nullable<int> struct” with a field of value 100.

Reflection late-bind invocation always deals with “object”; value type instances
are always getting boxed first before invocation. One of such APIs: PropertyInfo.SetValue(Object
obj, Object value, Object[]
index). There was
one question
in Microsoft Technical Forums: Exception is thrown when setting
DateTime value to a property with type Nullable<DateTime>. With this DCR change,
such late-bind calls can be written in a more natural way.

class C

{

   Int32? m_field;

   public Int32?
Property

   {

    get { return
m_field; }

    set { m_field =
value
; }

   }

}



PropertyInfo pi = typeof(C).GetProperty(“Property”);

C c = new C();

pi.SetValue(c, 100, null);        
// works


pi.SetValue(c, (Int32?)100, null);
// no longer have to write like this

After boxing, 100 and (Int32?)100 are the same thing: boxed Int32 object. There
is no more boxed object with type “Int32?”; late-invocation has to accept it and
set to Int32? property. Or think it this way, runtime can not tell where a boxed
Int32 object was originally from: either just Int32 instance or Int32? instance;
it is reasonable to allow setting a boxed Int32 object to field/property of either
Int32 or Int32? type (same for argument pass-in when doing method invocation)

pi.SetValue(c, null, null);
       // works

This actually worked before the DCR. And ,

Object ret = pi.GetValue(c, null);

ret will be either boxed Int32 object or simply null. There is no problem in casting
the result to Int32? type.

Here are some reflection behavior changes as a result of this DCR:

  • Probably the most important one is “typeof(T?).IsAssignableFrom(typeof(T)) == true”.
    PropertyInfo.SetValue/GetValue use this first to ensure the pass-in value is assignable
    to Nullable<T> property.
  • Object.GetType() will never get back any type of Nullable<T>. Joe had an interesting

    post
    on this.
  • Activator.CreateInstance could never be expected to return null before; with this
    DCR, it will return null when creating instance of type Nullable<T> but not
    providing non-null T value. For example, Activator.CreateInstance(typeof(Int32?))
    returns null.
Comments (4)

  1. Haibo Luo has a nice description of the changes made by Microsoft to nullable types and boxing for .NET 2.0 RTM: http://blogs.msdn.com/haibo_luo/archive/2005/08/23/455241.aspx This is good news for Sooda, because it would require very little work to properly support nullable types…

  2. barrkel says:

    I’ve written an expression compiler for my company than uses SQL semantics for all values loaded from our custom lightweight in-memory database. I use Nullable<> for all valuetypes.

    I’m wondering: are there many changes at the IL-level needed for this? Will

    ‘unbox.any Nullable`1[System.Int32]’

    still work as expected, even though the real type on the stack is a boxed Int32? And similarly, will ‘box Nullable`1[System.Int32]’ work?

  3. Haibo_Luo says:

    Hi barrkel – I talked to Peli (http://blog.dotnetwiki.org/), who tested this for JIT. He will give you an answer to your question.

  4. Peli says:

    The following snippet should answer naturally both questions

    static void Main()

    {

    int? i = 1;

    object o = i; // A

    int? j = (int?)o; // B

    int k = (int)o; // C

    }

    .method private hidebysig static void Main() cil managed

    {

    .entrypoint

    // Code size 32 (0x20)

    .maxstack 2

    .locals init (valuetype [mscorlib]System.Nullable`1<int32> V_0,

    object V_1,

    valuetype [mscorlib]System.Nullable`1<int32> V_2,

    int32 V_3)

    IL_0000: nop

    IL_0001: ldloca.s V_0

    IL_0003: ldc.i4.1

    IL_0004: call instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)

    IL_0009: nop

    IL_000a: ldloc.0

    IL_000b: box valuetype [mscorlib]System.Nullable`1<int32> // A

    IL_0010: stloc.1

    IL_0011: ldloc.1

    IL_0012: unbox.any valuetype [mscorlib]System.Nullable`1<int32> // B

    IL_0017: stloc.2

    IL_0018: ldloc.1

    IL_0019: unbox.any [mscorlib]System.Int32 // C

    IL_001e: stloc.3

    IL_001f: ret

    } // end of method Program::Main