Как foreach может определить тип элемента двумерного массива, если он не реализует IEnumerable?

Вопрос или проблема

Я не могу примирить эти два наблюдения:

  1. Двумерный массив (T[,]) нельзя присвоить переменной типа IEnumerable<T>
  2. В foreach(var item in (T[,])array2d) компилятор знает, что var это T.

Я думал, что foreach — это просто синтаксический сахар, который использует IEnumerable<T>.GetEnumerator() (в этом случае var является T), или IEnumerable.GetEnumerator() (в этом случае var является object).

Вот сценарий, который привел меня к этому вопросу. У меня есть метод, который принимает IEnumerable<Foo>.

int Average(IEnumerable<Foo> values)
{
    // много кода
    foreach (var foo in values)
    {
        var bar = foo.GetBar();
        // много кода
    }
    // много кода
    return result;
}

Foo[] array1d = /**/;
Average(array1d);

Затем я хотел передать двумерный массив этому методу, но он не компилируется; ошибка: CS1503 Argument 1: cannot convert from 'Foo[*,*]' to 'System.Collections.Generic.IEnumerable<Foo>'.

Foo[,] array2d = /**/;
Average(array2d);

Однако я обнаружил, что он компилируется, если я просто добавлю эту перегрузку. Когда я навожу курсор на var foo, компилятор знает, что это Foo и может найти Foo.GetBar().

void Average(Foo[,] values)
{
    // много кода
    foreach (var foo in values)
    {
        var bar = foo.GetBar();
        // много кода
    }
    // много кода
    return result;
}

К сожалению, это дублирует мой код. В качестве альтернативы я мог бы вызвать Average(array2d.Cast<Foo>());, но это добавляет еще один итератор, а я ищу производительность. Итератор, вероятно, не повлияет на производительность значительно, но мне кажется, что должен быть лучший способ.

Я думал, что foreach — это просто синтаксический сахар, который использует IEnumerable<T>.GetEnumerator() (в этом случае var это T), или IEnumerable.GetEnumerator() (в этом случае var это object).

Это не так — спецификация языка имеет конкретные правила для обработки массивов, а также IEnumerable<>. В C# 1 у нас вообще не было обобщений, поэтому для работы foreach с массивами должны были быть специфические правила.

Для более подробной информации смотрите спецификацию языка — это раздел 13.9.5 в спецификации C# 7, например.

В частности:

Если тип X выражения является типом массива, то существует неявное преобразование ссылки от X к интерфейсу IEnumerable (поскольку System.Array реализует этот интерфейс). Тип коллекции — это интерфейс IEnumerable, тип перечислителя — это интерфейс IEnumerator, а тип итерации — это тип элемента массива X.

Также стоит отметить следующее:

Реализация может реализовывать данный foreach_statement иначе; например, по причинам производительности, если поведение соответствует вышеуказанному расширению.

Если посмотреть на IL, созданный Roslyn (или старыми компиляторами MS) для цикла foreach по массиву, то GetEnumerator() вообще не вызывается – гораздо эффективнее использовать коды операций массива напрямую.

Ответ или решение

Вопрос о том, как оператор foreach может знать тип элемента двумерного массива, несмотря на то что он не реализует IEnumerable<T>, действительно интересен и часто вызывает путаницу.

Объяснение

  1. Общее понимание foreach и массивов:
    С оператором foreach в C# связано несколько нюансов, о которых стоит упомянуть. Когда вы используете foreach, компилятор не просто работает с IEnumerable<T>, а учитывает, что существует особая обработка массивов.

  2. Спецификация языка:
    В спецификации языка C# (например, в разделе 13.9.5 спецификации C# 7) указано, что:

    • Если тип выражения является массивом (X), существует неявное преобразование ссылки с X на интерфейс IEnumerable. Это означает, что сам массив, хоть и не реализует IEnumerable<T>, фактически может быть обработан в контексте IEnumerable, потому что все массивы в .NET унаследованы от класса System.Array, который реализует IEnumerable и IEnumerable<T>.
  3. Тип элемента:
    В этом контексте компилятор извлекает информацию о типе элементов массива (T для массива T[,]). В этом случае, когда вы используете foreach, компилятор знает, что var будет соответствовать типу элемента двумерного массива (Foo в вашем примере).

  4. Решение проблемы с IEnumerable<T>:
    Вы правильно заметили, что нельзя передать двумерный массив напрямую в метод, принимающий IEnumerable<Foo>. Однако, если вы хотите избежать дублирования кода и не хотите использовать дополнительный итератор, можно создать метод-расширение, который будет работать как обертка для вашего двумерного массива, чтобы иметь возможность обрабатывать его как IEnumerable<Foo>. Ниже приведен пример такого метода:

public static class ArrayExtensions
{
    public static IEnumerable<T> AsEnumerable<T>(this T[,] array)
    {
        foreach (var item in array)
        {
            yield return item;
        }
    }
}

После добавления этого метода, вы сможете вызывать ваш оригинальный метод Average следующим образом:

Foo[,] array2d = /**/;
Average(array2d.AsEnumerable());

Заключение

Таким образом, оператор foreach может обрабатывать двумерные массивы благодаря особенностям реализации System.Array и спецификации C#. Для работы с методами, принимающими IEnumerable<T>, рекомендуется использовать методы-расширения, позволяющие обрабатывать многомерные массивы, не дублируя код. Это обеспечит вам необходимую производительность и удобство использования.

Оцените материал
Добавить комментарий

Капча загружается...