This is documented in the C# specification under 8.8.4 The Foreach Statement (though sadly not in the online readable version):
If the type X of expression is an array type, then there is an implicit reference conversion
from X to the System.Collections.IEnumerable interface (since System.Array implements
this interface). The collection type is the System.Collections.IEnumerable interface,
the enumerator type is the System.Collections.IEnumerator interface, and the
element type is the element type of the array type X.
I've emphasized the point which documents the behavior you're asking about.
The array holds ints, thus the foreach statement will loop on ints as per the above rule.
The array is not "array of arrays of ints", it's "2d-array of ints". It's a collection of ints that have a rectangular shape indexing-wise, but it's still a collection of ints.
Note that a multidimensional array does not implement IEnumerable<T>
for its element type, but it does implement IEnumerable
, the non-generic interface.
As such, for the definition of a collection, the array is a collection of ints, even if it is multidimensional.
Note that arrays have a special place in the C# compiler, it will handle those specifically in a lot of cases, so there may be nothing you can infer from reflection, other than that it is an array.
To see some of the special handling that the compiler affords arrays, try executing this code in LINQPad, then click the IL tab:
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++) { }
}
You'll get this:
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
Note that there is no call to GetEnumerator
anywhere, which means that the foreach is not using GetEnumerator
. Instead it is rewritten to be a for-loop using an index, because the ways arrays are implemented in .NET, this is actually faster.