Почему foreach (var я in array2D) работает с многомерными массивами?

Учитывая следующий код С#:

   int[,] array2D = new int[10, 10];
   int sum = 0;

   foreach (var i in array2D)
   {
        sum += i;
   }

Возникает вопрос: что вызывает правильный тип i как int?

Это не является обходным, поскольку array2D является прямоугольным массивом. Он не реализует IEnumerable<int>. Он также реализует метод GetEnumerator(), который возвращает System.Collections.IEnumerator. Поэтому я ожидал бы, что i будет иметь тип object.

Мой код использует .net 4.03.

Связанный вопрос SO: Почему С# многомерные массивы не реализуют IEnumerable?.

Ответ 1

Массивы - это что-то, что было реализовано с помощью трюков и хаков. На самом деле Array не реализует IEnumerable<T> CLR читы, используя SZArrayHelper.

Связанный с этим вопрос и HansPassant имеют ответ здесь.

Также IL различен для кода foreach, сам компилятор С# генерирует разные IL для 2Darrays и одномерные массивы. Он даже не вызывает метод GetEnumerator грустно:(.

Подробная информация о волосах

Я не реализовал это, но это удивительно интересный набор работ в загрузчике, и несколько несколько взломанный код С#. Ты мог бы интересно, как кто-то определяет общие методы только для некоторых типов массивов, когда массивы генерируются типами, которые не имеют соответствующего источника код. С IList в V1 у нас был System.Array как базовый класс, который Int32 [] и Object [] подклассы, но мы не смогли добавить методы для Массив сам по себе, потому что реализация метода не имеет смысла на чем-либо, кроме SZArray. Как внутренняя реализация подробно, что никто не должен знать, в настоящее время у нас есть SZArrayHelper класс, который определяет все интересные тела метода, и мы передаем массив в качестве указателя "this". Это приводит к странный внутренний код, например:

sealed class SZArrayHelper 
{  
    internal int get_Count<T>() 
    {
          //! Warning: "this" is an array, not an SZArrayHelper. See comments above
         //! or you may introduce a security hole!
          T[] _this = this as T[];
          BCLDebug.Assert(_this!= null, "this should be a T[]");
          return _this.Length;  
    } 
}

К счастью, команда CLR сделала все сложное зло, чтобы сделать система типа работает так, как вы хотите, и это полностью которые вам не нужно знать. Иногда строить интересная система типов эффективно, вам нужно играть некоторые игры, которые заставили бы вашего профессора компьютерных наук охотиться на вас вниз.

Конечным результатом наших усилий является то, что вы можете назвать общий алгоритм который потребляет IList и передает ему общую коллекцию (например, List) или массив (например, String []) и получить эффективный элемент доступ без дополнительной логики, вызванной вашим лицом. У вас нет сделать любые глупые классы адаптеров или что-то в этом роде.

Выше абзаца из Связанная статья, в которой говорится о SZArray.

Просто они тайны. Я хотел бы поделиться тем, что знаю до сих пор, я знаю, что это не дает ответа, но я надеюсь, что это поможет.

Ответ 2

Это описано в спецификации С# в разделе 8.8.4. Утверждение Foreach (хотя, к сожалению, не в онлайновой читаемой версии):

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

Я подчеркнул тот момент, который документирует поведение, о котором вы просите.

В массиве содержится ints, поэтому оператор foreach будет зацикливаться на ints согласно приведенному выше правилу.

Массив не является "массивом массивов из int", это "2d-массив из int". Это набор индексов, которые имеют прямоугольную форму индексирования, но это все еще коллекция ints.

Обратите внимание, что многомерный массив не реализует IEnumerable<T> для своего типа элемента, но реализует IEnumerable, не общий интерфейс.

Таким образом, для определения коллекции массив представляет собой набор int, даже если он является многомерным.

Обратите внимание, что массивы имеют особое место в компиляторе С#, они будут обрабатывать их специально во многих случаях, поэтому не может быть ничего, что вы можете сделать из отражения, кроме того, что это массив.

Чтобы увидеть некоторые из специальных действий, которые компилятор предоставляет массивам, попробуйте выполнить этот код в LINQPad, затем щелкните вкладку IL:

void Main()
{
}

public void A()
{
    int[] a = new int[10];

    foreach (var x in a) { }
}

public void B()
{
    int[] a = new int[10];

    for (int i = 0; i < a.Length; i++) { }
}

Вы получите следующее:

IL_0000:  ret         

A:
IL_0000:  ldc.i4.s    0A 
IL_0002:  newarr      System.Int32
IL_0007:  stloc.0     // a
IL_0008:  ldloc.0     // a
IL_0009:  stloc.1     // CS$6$0000
IL_000A:  ldc.i4.0    
IL_000B:  stloc.2     // CS$7$0001
IL_000C:  br.s        IL_0016
IL_000E:  ldloc.1     // CS$6$0000
IL_000F:  ldloc.2     // CS$7$0001
IL_0010:  ldelem.i4   
IL_0011:  pop         
IL_0012:  ldloc.2     // CS$7$0001
IL_0013:  ldc.i4.1    
IL_0014:  add         
IL_0015:  stloc.2     // CS$7$0001
IL_0016:  ldloc.2     // CS$7$0001
IL_0017:  ldloc.1     // CS$6$0000
IL_0018:  ldlen       
IL_0019:  conv.i4     
IL_001A:  blt.s       IL_000E
IL_001C:  ret         

B:
IL_0000:  ldc.i4.s    0A 
IL_0002:  newarr      System.Int32
IL_0007:  stloc.0     // a
IL_0008:  ldc.i4.0    
IL_0009:  stloc.1     // i
IL_000A:  br.s        IL_0010
IL_000C:  ldloc.1     // i
IL_000D:  ldc.i4.1    
IL_000E:  add         
IL_000F:  stloc.1     // i
IL_0010:  ldloc.1     // i
IL_0011:  ldloc.0     // a
IL_0012:  ldlen       
IL_0013:  conv.i4     
IL_0014:  blt.s       IL_000C
IL_0016:  ret         

Обратите внимание, что вызов GetEnumerator в любом месте отсутствует, что означает, что foreach не использует GetEnumerator. Вместо этого он переписывается как цикл for, используя индекс, потому что способы массивов реализованы в .NET, это происходит быстрее.

Ответ 3

Это не очень интуитивно понятно, потому что это многомерный массив, но это ожидаемое поведение для перечисления такого массива и оно задокументировано

С многомерными массивами вы можете использовать один и тот же метод для итерации через элементы, например:

int[,] numbers2D = new int[3, 2] { { 9, 99 }, { 3, 33 }, { 5, 55 } };
// Or use the short form: 
// int[,] numbers2D = { { 9, 99 }, { 3, 33 }, { 5, 55 } }; 

foreach (int i in numbers2D)
{
    System.Console.Write("{0} ", i);
}
// Output: 9 99 3 33 5 55