Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why IsAssignableFrom return false when comparing a nullable against an interface?

Tags:

c#

types

The following call in C# returns false :

typeof(IComparable).IsAssignableFrom(typeof(DateTime?))

However, the following line is perfectly valid :

IComparable comparable = (DateTime?)DateTime.Now;

Why is it so ?

Is it because nullable types are backed using Nullable<T>and the fact the first generic argument implements an interface does not imply that the Nullable class also implement that interface ? (eg : List<Foo> does not implement interfaces that Foo implement)

EDIT : I think the line above compile because when boxing an nullable type, only the underlying type is boxed as explained here : https://msdn.microsoft.com/en-us/library/ms228597.aspx

like image 635
tigrou Avatar asked Oct 10 '16 10:10

tigrou


2 Answers

The reason for this behaviour is that IsAssignableFrom() does not consider the special boxing conversions that the compiler emits for conversion of nullable types.

Note that you don't actually need the cast in your question.

Instead of

 IComparable comparable = (DateTime?)DateTime.Now;

you can write:

DateTime? test = DateTime.Now;
IComparable comparable = test;

The first of those lines compiles because Nullable<T> provides an implicit conversion operator:

public static implicit operator Nullable<T> (
    T value
)

The second line causes the compiler to emit a box instruction:

L_000e: box [mscorlib]System.Nullable`1<valuetype [mscorlib]System.DateTime>

This boxing operation is covered by section 6.1.7 of the C# language spec, Boxing conversions (this covers boxing conversions for nullable types specifically), which states:

A boxing conversion permits a value-type to be implicitly converted to a reference type. A boxing conversion exists from any non-nullable-value-type to object and dynamic, to System.ValueType and to any interface-type implemented by the non-nullable-value-type. Furthermore an enum-type can be converted to the type System.Enum.

A boxing conversion exists from a nullable-type to a reference type, if and only if a boxing conversion exists from the underlying non-nullable-value-type to the reference type.

A value type has a boxing conversion to an interface type I if it has a boxing conversion to an interface type I0 and I0 has an identity conversion to I.

A value type has a boxing conversion to an interface type I if it has a boxing conversion to an interface or delegate type I0 and I0 is variance-convertible (§13.1.3.2) to I.

Boxing a value of a non-nullable-value-type consists of allocating an object instance and copying the value-type value into that instance. A struct can be boxed to the type System.ValueType, since that is a base class for all structs (§11.3.2).

This is what is giving rise to the boxing operation above. I've bolded and italicised the most pertinent point.

Also see this link (supplied by the OP): https://msdn.microsoft.com/en-us/library/ms228597.aspx

like image 104
Matthew Watson Avatar answered Oct 24 '22 09:10

Matthew Watson


Nullables are special.

object boxedNullable = new decimal?(42M);

Console.WriteLine(boxedNullable.GetType().Name); // Decimal

When you box a nullable value, what you actually do is box the underlying value, not the nullable. So default(decimal?) will give you just null (rather than a "no-value nullable"), and new decimal?(42M) will give you a boxed decimal.

When you cast a value type to an interface, it must be boxed - so your second line actually changes the nullable to a boxed DateTime. DateTime? doesn't implement IComparable, but DateTime does - and that's what you're ultimately casting to the interface, because the value type must be boxed first.

This is defined in the ECMA specification, I.8.2.4 Boxing and unboxing of values:

All value types have an operation called box. Boxing a value of any value type produces its boxed value; i.e., a value of the corresponding boxed type containing a bitwise copy of the original value. If the value type is a nullable type—defined as an instantiation of the value type System.Nullable<T>—the result is a null reference or bitwise copy of its Value property of type T, depending on its HasValue property (false and true, respectively). All boxed types have an operation called unbox, which results in a managed pointer to the bit representation of the value.

Your best bet is to use either x is IComparable or x as IComparable to find if some type implements IComparable; using reflection exposes you to a lot of the tiny little quirks of both .NET and C# (and whatever other language the guy who wrote the code used).

like image 32
Luaan Avatar answered Oct 24 '22 09:10

Luaan