LJM
LJM

Reputation: 6444

C# .NET Unexpected Typing - Expanded foreach explanation

As I was refactoring some code this morning, I noticed some weird behavior. I was iterating over a collection of type A. The declaration and usage of the Enumerable were split (I was declaring and defining a variable using some Linq, then iterating over it later via foreach). However, when I changed the type of the enumerable from IEnumerable<A> to IEnumerable<B>, I left the foreach as the following where enumerable was of type IEnumerable<B>.

IEnumerable<B> enumerable = someEnumerableOfB    
foreach(A a in enumerable)

Following is a contrived example of the behavior I found:

    IEnumerable<IEnumerable> enumerables = Enumerable.Range(1, 5).Select(x => new List<int> { x });
    foreach (StringComparer i in enumerables) //this compiles
    {
        //do something here
    }

    foreach (int i in enumerables) //this doesn't compile
    {
        //do something here
    }

    IEnumerable<StringBuilder> stringBuilders = Enumerable.Range(1, 5).Select(x => new StringBuilder(x.ToString()));
    foreach (FileStream sb in stringBuilders) //this doesn't compile
    {
        //do something here                
    }

I was surprised to see the first one compile. Can someone explain exactly why this works? I assume it has something to do with the fact that the IEnumerable is of an interface, but I can't explain it.

Upvotes: 4

Views: 230

Answers (2)

R. Martinho Fernandes
R. Martinho Fernandes

Reputation: 234424

According to the algorithm described section §15.8.4. of the specification, the compiler will expand the foreach into the following:

{
    IEnumerator<IEnumerable> e = ((IEnumerable<IEnumerable>)(x)).GetEnumerator(); 
    try
    {
        StringComparer v; 
        while (e.MoveNext())
        {
            v = (StringComparer)(IEnumerable)e.Current; // (*)
            // do something here
        }    
    } 
    finally
    {
        // Dispose of e
    }
}

The line I've marked with an asterisk is the reason why it compiles for the first and not for the second. That is a valid cast because you can have a subclass of StringComparer that implements IEnumerable. Now change it to:

v = (int)(IEnumerable)e.Current; // (*)

And it doesn't compile, because this is not a valid cast: int does not implement IEnumerable, and it can't have any subclasses.

Upvotes: 4

Polity
Polity

Reputation: 15130

Because the compiler cant know whats in the Enumerable but it knows that it cant be a value type (e.g. int).

foreach (StringComparer i in enumerables) will compile since StringComparer is a reference type and for the compilers sake might just be in enumerables.

Upvotes: 1

Related Questions