40

Consider this contrived, trivial example:

    var foo = new byte[] {246, 127};
    var bar = foo.Cast<sbyte>();
    var baz = new List<sbyte>();
    foreach (var sb in bar)
    {
        baz.Add(sb);
    }
    foreach (var sb in baz)
    {
        Console.WriteLine(sb);
    }

With the magic of Two's Complement, -10 and 127 is printed to the console. So far so good. People with keen eyes will see that I am iterating over an enumerable and adding it to a list. That sounds like ToList:

    var foo = new byte[] {246, 127};
    var bar = foo.Cast<sbyte>();
    var baz = bar.ToList();
    //Nothing to see here
    foreach (var sb in baz)
    {
        Console.WriteLine(sb);
    }

Except that does not work. I get this exception:

Exception type: System.ArrayTypeMismatchException

Message: Source array type cannot be assigned to destination array type.

I find this exception very peculiar because

  1. ArrayTypeMismatchException - I'm not doing anything with arrays, myself. This seems to be an internal exception.
  2. The Cast<sbyte> works fine (as in the first example), it's when using ToArray or ToList the problem presents itself.

I'm targeting .NET v4 x86, but the same occurs in 3.5.

I don't need any advice on how to resolve the problem, I've already managed to do that. What I do want to know is why is this behavior occurring in the first place?

EDIT:

Even weirder, adding a meaningless select statement causes the ToList to work correctly:

var baz = bar.Select(x => x).ToList();
vcsjones
  • 138,677
  • 31
  • 291
  • 286
  • With the `Select` it results in `{ -10, 127 }`. There is a casting issue here. Interesting error message for sure. – asawyer Jun 14 '12 at 18:53
  • @Lieven yes, that much I gathered, by why does `Select(x => x)` before `ToList` correct it? That is a meaningless projection because the same thing is projected back. – vcsjones Jun 14 '12 at 18:57
  • 2
    I have an explanation... it's just taking a little while to write out. Great question. – Jon Skeet Jun 14 '12 at 18:58
  • 2
    @vcsjones: Because then the array helper won't be used anymore most likely - using `Select` you are projecting to an `IEnumerable` – BrokenGlass Jun 14 '12 at 19:00
  • @vcsjones - I was not trying to be pedantic *(it comes natural)* but I thought it might be of help to you. I'll remove the comment to not clutter the conversation. *(waiting impatiently for Jon Skeet's answer)* – Lieven Keersmaekers Jun 14 '12 at 19:03
  • 2
    +1 Great question. I love questions that invovle interesting edge cases. – JDB Jun 14 '12 at 19:11

1 Answers1

32

Okay, this really depends on a few oddities combined:

  • Even though in C# you can't cast a byte[] to an sbyte[] directly, the CLR allows it:

    var foo = new byte[] {246, 127};
    // This produces a warning at compile-time, and the C# compiler "optimizes"
    // to the constant "false"
    Console.WriteLine(foo is sbyte[]);
    
    object x = foo;
    // Using object fools the C# compiler into really consulting the CLR... which
    // allows the conversion, so this prints True
    Console.WriteLine(x is sbyte[]);
    
  • Cast<T>() optimizes such that if it thinks it doesn't need to do anything (via an is check like the above) it returns the original reference - so that's happening here.

  • ToList() delegates to the constructor of List<T> taking an IEnumerable<T>

  • That constructor is optimized for ICollection<T> to use CopyTo... and that's what's failing. Here's a version which has no method calls other than CopyTo:

    object bytes = new byte[] { 246, 127 };
    
    // This succeeds...
    ICollection<sbyte> list = (ICollection<sbyte>) bytes;
    
    sbyte[] array = new sbyte[2];
    
    list.CopyTo(array, 0);
    

Now if you use a Select at any point, you don't end up with an ICollection<T>, so it goes through the legitimate (for the CLR) byte/sbyte conversion for each element, rather than trying to use the array implementation of CopyTo.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 2
    Yeah, I noticed that the value type of bar at run time was byte[], rather than System.Linq.Enumerable.CastIterator. Thanks for explaining! – JDB Jun 14 '12 at 19:07
  • after reading the question and answer, is that mean .ToList will need to be use in type-safe conversion? – Turbot Jun 14 '12 at 20:22
  • 1
    @Turbot: I don't understand your comment. Could you rephrase it? – Jon Skeet Jun 14 '12 at 20:24
  • Hm, now I see. The `Cast` was never doing anything in either the first or second example, rather the conversion was being handled either by virtue of adding it to the list and the CLR handling the cast. – vcsjones Jun 14 '12 at 20:56
  • @JonSkeet Oops, It is nothing to do with type-safe conversion. It is just a different _Treatment_ from C# compiler and CLR handling the conversion? – Turbot Jun 14 '12 at 21:30
  • @Turbot: Well, it's a mixture of that and also *library* support. Fundamentally the question is whether you should be able to treat a `byte[]` as an `sbyte[]` (i.e. the same object). – Jon Skeet Jun 15 '12 at 05:55
  • This is clearly not the most clear explanation. And I still cannot cast `IntPtr[]` to `List`. – SerG Feb 05 '15 at 16:39
  • @SerG: Why would you expect to be able to? Where has anyone else mentioned `IntPtr`? Of course, if you have a clearer explanation, I'd encourage you to add your own answer. – Jon Skeet Feb 05 '15 at 16:59
  • I just get the same error doing similar things and thought it's analogy. Don't take offence, of course I have no explanation. – SerG Feb 05 '15 at 17:14
  • @SerG: It doesn't *sound* like the same situation. Converting between int and IntPtr is very different to converting between sbyte and byte. Sounds like you should as a new question. – Jon Skeet Feb 05 '15 at 17:18