views:

145

answers:

4

Possible Duplicate:
Adding null to a List<bool?> cast as an IList throwing an exception.

List<int?> listONullables = new List<int?>();
IList degenericed = listONullables;

// This works fine
listONullables.Add(null); 

// Run time exception:
// "The value "" is not of type "System.Nullable`1[System.Int32]"
// and cannot be used in this generic collection.  Parameter name: value"
degenericed.Add(null);

// Also does not work.  Same exception
degenericed.Add((int?)null);

// Also does not work
// EDIT: I was mistaken, this does work
degenericed.Add((int?)1);

// Also does not work
// EDIT: I was mistaken, this does work
degenericed.Add(1);

See the comments in the above code.

I sort of understand the reasons for this (when you cast away the generics the runtime does the best it can with limited information). I'm just wondering if there's a way around this, even if it's a bit of a hack.

The problem sprang up when I tried having the generic version of a function use the same private implementation as a non generic version, so I can work around it if necessary (have two very similar implementations), but obviously it's better if I can figure this out.

EDIT: The last two entries I have above do NOT fail like I originally said. But the first two do. I've added comments to that effect in the code above.

+4  A: 

To elaborate on the discussion in the comments, it seems that in List<T>.IList.Add in 4.0, there is:

ThrowHelper.IfNullAndNullsAreIllegalThenThrow<T>(item, ExceptionArgument.item);
try
{
    this.Add((T) item);
}
catch (InvalidCastException)
{
    ThrowHelper.ThrowWrongValueTypeArgumentException(item, typeof(T));
}

And 2.0 has VerifyValueType which simply checks the IsCompatibleObject method:

VerifyValueType(item);

...

private static bool IsCompatibleObject(object value) {
    if( (value is T) || ( value == null && !typeof(T).IsValueType) ) {
        return true; 
    }
    return false; 
} 

The latter is written in a simplistic fashion. value is not T (because null is not the same as Nullable<int>.HasValue = false). Also, as @LBushkin notes, typeof(T).IsValueType will return true for Nullable<int> and so the right-hand-side also evaluates to false.

Kirk Woll
I believe the problem here is that `!typeof(T).IsValueType` evaluates to false when `T` is `Nullable<int>` and `value is T` also evaluates for false. Consequently, the check fails, and you cannot add a null via this implementation. The .NET 4.0 implementation simply delegates to the generic `List<T>.Add` implementation which correctly handles this case.
LBushkin
@LBushkin, you're right, that needed addressing as well. Will update.
Kirk Woll
@Kirk: Using `degenericed.Add(new Nullable<int>())` also fails since it's equivalent to `degenericed.Add((int?)null)`. The end result is no different to passing plain `null` into the `Add` method.
LukeH
@LukeH, ah, I think I see the confusion. The OP is incorrect that degenericed.Add((int?)null) would fail. (And you are incorrect that my solution would fail. ;) )
Kirk Woll
Nope. `Add(null)` fails, `Add((int?)null)` fails and `Add(new Nullable<int>())` fails. The latter two calls generate *exactly* the same IL. I'm using the 32-bit version of .NET 3.5sp1.
LukeH
@LukeH, sorry for the extended discussion, but you're correct. Will update the answer.
Kirk Woll
A: 
Foovanadil
I don't think this has anything to do with the variance changes in .NET 4 - it's just a bug in older versions of the framework.
LukeH
A: 

try to change the line:

IList degenericed = listONullables;

by this:

IList<int?> degenericed = listONullables;
Minoru T.
A: 

This is a bug in the 3.5 framework (and probably earlier versions too). The rest of this answer relates to .NET 3.5, although the comments suggest that the bug has been fixed in version 4 of the framework...

When you pass a value-type to the IList.Add method it will be boxed as an object due to the IList interface being non-generic. The one exception to this rule are null nullable types which are converted (not boxed) to plain null.

The IList.Add method on the List<T> class checks that the type you're trying to add is actually a T, but the compatibility check doesn't take null nullable types into account:

When you pass null, the compatibility check knows that your list is a List<int?> and knows that int? is a value-type, but -- here's the bug -- throws an error because it also "knows" that value-types cannot possibly be null, ergo the null that you passed cannot possibly be an int?.

LukeH
@LukeH, in fact it does not throw an error for `1`.
Kirk Woll
@Kirk: Oops, fixed!
LukeH