Вопрос или проблема
Я не могу примирить эти два наблюдения:
- Двумерный массив (
T[,]
) нельзя присвоить переменной типаIEnumerable<T>
- В
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>
, действительно интересен и часто вызывает путаницу.
Объяснение
-
Общее понимание
foreach
и массивов:
С операторомforeach
в C# связано несколько нюансов, о которых стоит упомянуть. Когда вы используетеforeach
, компилятор не просто работает сIEnumerable<T>
, а учитывает, что существует особая обработка массивов. -
Спецификация языка:
В спецификации языка C# (например, в разделе 13.9.5 спецификации C# 7) указано, что:- Если тип выражения является массивом (
X
), существует неявное преобразование ссылки сX
на интерфейсIEnumerable
. Это означает, что сам массив, хоть и не реализуетIEnumerable<T>
, фактически может быть обработан в контекстеIEnumerable
, потому что все массивы в .NET унаследованы от классаSystem.Array
, который реализуетIEnumerable
иIEnumerable<T>
.
- Если тип выражения является массивом (
-
Тип элемента:
В этом контексте компилятор извлекает информацию о типе элементов массива (T
для массиваT[,]
). В этом случае, когда вы используетеforeach
, компилятор знает, чтоvar
будет соответствовать типу элемента двумерного массива (Foo
в вашем примере). -
Решение проблемы с
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>
, рекомендуется использовать методы-расширения, позволяющие обрабатывать многомерные массивы, не дублируя код. Это обеспечит вам необходимую производительность и удобство использования.