Kung Pao Chicken
Kung Pao Chicken

Reputation: 136

Exception for calling ToList() after conversion from uint[] to int[] in C#

I get an exception System.ArrayTypeMismatchException: Source array type cannot be assigned to destination array type for this code snippet:

var uints = GetArray();
if (uints is int[])
{
    var list = ((int[])uints).ToList(); // fails when call ToList()
}

private Array GetArray()
{
    var result = new uint[] { uint.MaxValue, 2, 3, 4, 5 };
    return result;
}

Then I resort to Jon's answer in Why does "int[] is uint[] == true" in C#, it told me that because of GetArray() returns an Array, the conversion was postponed in runtime, and CLR allows this kind of int[] to uint[](vice versa) cast. And it actually works fine if I check values after conversion:

foreach (var i in ((int[])units))
{
    System.Console.WriteLine(i.GetType());
    System.Console.WriteLine(i);
}

I would get:

System.Int32
-1
System.Int32
2
//skipped...

However I kind of get confused why it would fail when calling ToList(), as this code below will work fine?

internal class Animal
{
}

internal class Cat : Animal
{
}

var cats = new Cat[] { new Cat(), new Cat() };
List<Animal> animals = ((Animal[])cats).ToList(); //no exception

Upvotes: 9

Views: 469

Answers (2)

csharpfolk
csharpfolk

Reputation: 4280

What is happening here? Let's start with foreach loop, C# compiler optimizes them heavily especially when you use them with arrays - they are basically changes to normal for(int i = 0; i < length; i++) { } - so the example with

foreach (var i in ((int[])units))
{
    System.Console.WriteLine(i.GetType());
    System.Console.WriteLine(i);
}

cannot be trusted, BTW foreach can also perform cast on element type it is better to try generic Array.GetValue method:

int[] x = ((int[])uints);
Console.WriteLine(x.GetValue(0).GetType()); // System.UInt32

Console.WriteLine(x[0].GetType()); // System.Int32

So even access x[0] can return already casted value but Array.GetValue returned what was already there an uint.

Let's do another experiment:

Console.WriteLine(x.GetType()); // System.UInt32[]

Console.WriteLine(uints.GetType()); // System.UInt32[]

Console.WriteLine(Object.ReferenceEquals(x, uints)); // True

This assures us that cast in var x = (int[])uints is a NOP - no operation, it does nothing at all. Especially 3th line show us the we get exactly the same instance.

Now inside List constructor we have lines

_items = new T[count];
c.CopyTo(_items, 0);

that actually throw Array mismatch exception.

But why this exception wasn't thrown earlier e.g. when GetEnumerator() is called I don't know myself, I expected exception to be thrown on lines

x.GetEnumerator()

because types IEnumerable<int> and IEnumerable<uint> are not compatible but none was - maybe because .NET returns here System.Array+SZArrayEnumerator that is the same for every value type array.

EDIT: (After example with Cat) Array covariance in C# assures us that any array of reference types can be assigned to object[] and that array of type Subclass[] can be assigned to BaseClass[]. The case with value types is different since they can have different sizes and/or conversion behaviours (uint vs int).

ToList uses internally Array.Copy call, when we look at Array.Copy implementation in CRL: https://github.com/dotnet/coreclr/blob/32f0f9721afb584b4a14d69135bea7ddc129f755/src/classlibnative/bcltype/arraynative.cpp#L328 we see that array can be copied only if value types have compatible singess which is checked by another util function https://github.com/dotnet/coreclr/blob/32f0f9721afb584b4a14d69135bea7ddc129f755/src/vm/invokeutil.h#L196

The other question is why it was implemented this way...

Upvotes: 4

Abion47
Abion47

Reputation: 24661

You are trying to cast the array to int[] when it is a uint[]. That by itself is fine, since both int[] and uint[] are of type Array, and casting an Array object of type uint[] as an int[] (or vice versa) is allowed (for some reason).

The problem comes when you try to then convert it to a List. The method is trying to create a List of type int because that's the type you specified, but that's not what the type of the array actually is.

Upvotes: 8

Related Questions