10

Given the following C# code:

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

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

The question is: what is causing the type of i to be correctly inferred as an int?

This is not at all obviuous, as array2D is a rectangular array. It does not implement IEnumerable<int>. It also implements a GetEnumerator() method, which returns a System.Collections.IEnumerator. I would therefore expect that i would be of type object.

My code is using .net 4.03.

Related SO question: Why do C# Multidimensional arrays not implement IEnumerable?.

Community
  • 1
  • 1

3 Answers3

5

Arrays are somethings which was implemented using tricks and hacks. Actually Array doesn't implement IEnumerable<T> CLR cheats us using SZArrayHelper.

Related question of mine and HansPassant have a related answer here.

Also IL is different for both foreach code, c# compiler itself generates different IL for 2Darrays and single dimensional arrays. It doesn't even call GetEnumerator method sadly :(.

Hairy Implementation Details

I didn't implement this, but it's an amazingly interesting set of work in the loader, and some somewhat hacky looking C# code. You might wonder how someone defines generic methods on only some array types, when arrays are generated types that have no corresponding source code. With IList in V1, we had System.Array as a base class that Int32[] and Object[] subclassed, but we couldn't add the methods to Array itself, because the method implementations wouldn't make sense on anything other than an SZArray. As an internal implementation detail that no one needs to know, we currently have an SZArrayHelper class that defines all the interesting method bodies, and we pass an array to the method as the "this" pointer. This leads to some very strange internal code, like 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;  
    } 
}

Fortunately, the CLR team has done all the complex evilness to make the type system work the way you want, and this is a completely internal detail that you don't need to know about. Sometimes to build an interesting type system in an efficient manner, you need to play some games that would make your computer science professor hunt you down.

The end result of our efforts is that you can call a generic algorithm that consumes IList and pass it a generic collection (like List) or an array (like String[]) and get efficient element accesses with no additional logic forced in your face. You don't have to make any silly adapter classes or anything like that.

Above paragraph is from Related article which talks about SZArray.

Simply they are Mystery. I'd like to share what I know so far, I know this doesn't provide answer but I hope this helps.

Community
  • 1
  • 1
Sriram Sakthivel
  • 72,067
  • 7
  • 111
  • 189
3

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.

Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
  • I'm upvoting your answer, hopefully downvoters will leave a comment. – ken2k Mar 07 '14 at 14:33
  • 1
    Sorry but this answer does not explain anything. –  Mar 07 '14 at 14:36
  • @jdv-JandeVaan It explains the multi-dimensional array is an array of int, not an array of array, and as such the enumeration returns int as expected. – ken2k Mar 07 '14 at 14:39
  • added note from the C# specification – Lasse V. Karlsen Mar 07 '14 at 14:41
  • Thanks for expanding on your initial answer. I see now that you were trying to explain that the foreach will not enumerate over rows of the matrix. But I was not expecting this to happen. I know that multidimensional arrays are not arrays of arrays. I was mostly puzzled that GetEnumerator() returns an untyped enumerator, and the foreach was using the right type anyway. begins to look like special handling. –  Mar 07 '14 at 14:59
  • Added example of special handling – Lasse V. Karlsen Mar 07 '14 at 15:03
2

It's not very intuitive because it's a multi-dimensional array, but it's the expected behavior for the enumeration of such an array, and it's documented:

With multidimensional arrays, you can use the same method to iterate through the elements, for example:

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
BartoszKP
  • 34,786
  • 15
  • 102
  • 130
ken2k
  • 48,145
  • 10
  • 116
  • 176
  • +1, this is the correct answer. Relevant citations from the C# specification could also improve this answer; 8.4.4 ("The foreach statement") contains the details. –  Mar 07 '14 at 14:34
  • +1 for the documentation link. However the mechanism is still unclear. I have always understood that the standard foreach depends on 'GetEnumerator'. –  Mar 07 '14 at 14:39
  • Running gettype() on it gives us System.Int32 [,] – Martin Milan Mar 07 '14 at 14:59