Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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
like image 413
Kung Pao Chicken Avatar asked Nov 15 '16 07:11

Kung Pao Chicken


2 Answers

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.

like image 83
Abion47 Avatar answered Oct 06 '22 01:10

Abion47


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...

like image 32
csharpfolk Avatar answered Oct 06 '22 00:10

csharpfolk