Почему массив в роли IEnumerable игнорирует отложенное выполнение?

Я столкнулся с этой проблемой сегодня, и я не понимаю, что происходит:

enum Foo
{
    Zero,
    One,
    Two
}

void Main()
{
    IEnumerable<Foo> a = new Foo[]{ Foo.Zero, Foo.One, Foo.Two};
    IEnumerable<Foo> b = a.ToList();

    PrintGeneric(a.Cast<int>());
    PrintGeneric(b.Cast<int>());

    Print(a.Cast<int>());
    Print(b.Cast<int>());
}

public static void PrintGeneric<T>(IEnumerable<T> values){
    foreach(T value in values){
        Console.WriteLine(value);
    }
}

public static void Print(IEnumerable values){
    foreach(object value in values){
        Console.WriteLine(value);
    }
}

Вывод:

0
1
2
0
1
2
Zero
One
Two
0
1
2

Я знаю, что Cast() приведет к отсроченному исполнению, но похоже, что приведение его в IEnumerable приводит к тому, что отложенное выполнение теряется, и только если фактическая коллекция реализации представляет собой массив.

Почему перечисление значений в методе Print приводит к тому, что enum используется для int для коллекции List<Foo>, но не Foo[]?

Ответ 1

Это из-за оптимизации, которая, к сожалению, немного нарушена перед неожиданными преобразованиями CLR.

На уровне CLR есть ссылочное преобразование от Foo[] до int[] - вам вообще не нужно бросать каждый объект. Это неверно на уровне С#, но оно находится на уровне CLR.

Теперь Cast<> содержит оптимизацию, чтобы сказать "если я уже имею дело с коллекцией правильного типа, я могу просто вернуть ту же ссылку назад" - эффективно:

if (source is IEnumerable<T>)
{
    return source;
}

Итак a.Cast<int> возвращает a, который является Foo[]. Это прекрасно, когда вы передаете его на PrintGeneric, потому что тогда есть неявное преобразование в T в цикле foreach. Компилятор знает, что тип IEnumerator<T>.Current равен T, поэтому соответствующий слот стека имеет тип T. Скомпилированный код JIT-аргумента для каждого типа будет "делать правильную вещь" при обработке значения как int, а не как Foo.

Однако, когда вы передаете массив как IEnumerable, свойство Current в IEnumerator имеет тип object, поэтому каждое значение будет помещено в поле и передано в Console.WriteLine(object) - и вставляется в коробку объект будет иметь тип Foo, а не int.

Вот пример кода, чтобы показать первую часть этого - остальное немного проще понять, я считаю, после того, как вы прошли мимо:

using System;
using System.Linq;

enum Foo { }

class Test
{
    static void Main()
    {
        Foo[] x = new Foo[10];
        // False because the C# compiler is cocky, and "optimizes" it out
        Console.WriteLine(x is int[]);

        // True because when we put a blindfold in front of the compiler,
        // the evaluation is left to the CLR
        Console.WriteLine(((object) x) is int[]);

        // Foo[] and True because Cast returns the same reference back
        Console.WriteLine(x.Cast<int>().GetType());
        Console.WriteLine(ReferenceEquals(x, x.Cast<int>()));
    }
}

Вы увидите то же самое, если попытаетесь пройти между uint[] и int[]. Кстати,