Search code examples
c#multidimensional-arrayforeachvarc#-12.0

How can foreach know the item type of a 2D array when it's doesn't implement IEnumerable<T>?


I can't seem to reconcile these two observations:

  1. A 2D array (T[,]) can't be assigned to a variable of type IEnumerable<T>
  2. In foreach(var item in (T[,])array2d), the compiler knows that var is T.

I thought foreach was just syntactic sugar that uses IEnumerable<T>.GetEnumerator() (in which case var is T), or IEnumerable.GetEnumerator() (in which case var is object).

Here is the scenario that led me to this question. I have a method that takes an IEnumerable<Foo>.

int Average(IEnumerable<Foo> values)
{
    // lots of code
    foreach (var foo in values)
    {
        var bar = foo.GetBar();
        // lots of code
    }
    // lots of code
    return result;
}

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

I then wanted to pass a 2D array to this method, but it doesn't compile; the error is CS1503 Argument 1: cannot convert from 'Foo[*,*]' to 'System.Collections.Generic.IEnumerable<Foo>'.

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

However, I discovered that it does compile if I just add this overload. When I mouse over var foo, the compiler knows that it is a Foo, and is able to find Foo.GetBar().

void Average(Foo[,] values)
{
    // lots of code
    foreach (var foo in values)
    {
        var bar = foo.GetBar();
        // lots of code
    }
    // lots of code
    return result;
}

Unfortunately, this duplicates my code. I could alternatively call Average(array2d.Cast<Foo>());, but this adds an extra iterator, and I'm looking for performance. The iterator probably won't affect performance significantly, but it feels like there has to be a better way.


Solution

  • I thought foreach was just syntactic sugar that uses IEnumerable<T>.GetEnumerator() (in which case var is T), or IEnumerable.GetEnumerator() (in which case var is object).

    That's not the case - the language specification has specific rules to handle arrays as well as IEnumerable<>, basically. Back in C# 1, we didn't have generics at all, so for foreach to work over arrays there had to be specific rules.

    For much more detail, see the language specification - it's section 13.9.5 in the C# 7 spec for example.

    In particular:

    If the type X of expression is an array type then there is an implicit reference conversion from X to the IEnumerable interface (since System.Array implements this interface). The collection type is the IEnumerable interface, the enumerator type is the IEnumerator interface and the iteration type is the element type of the array type X.

    It's also worth noting this:

    An implementation is permitted to implement a given foreach_statement differently; e.g., for performance reasons, as long as the behavior is consistent with the above expansion.

    If you look at the IL created by Roslyn (or older MS compilers) for a foreach loop over an array, it doesn't call GetEnumerator() at all - it's much more efficient to use the array op codes directly.