4

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.

LJM
  • 6,284
  • 7
  • 28
  • 30

2 Answers2

4

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.

R. Martinho Fernandes
  • 228,013
  • 71
  • 433
  • 510
1

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.

Polity
  • 14,734
  • 2
  • 40
  • 40
  • If this is indeed a covariance/contravariance loophole, please explain in more detail. Specifically, looking at your explanation, I have no way of knowing why the third example doesn't compile. – LJM Apr 11 '11 at 15:58
  • Right. See also [this answer](http://stackoverflow.com/questions/5413830/c4-0-int-a-real-subtype-of-object-covariance-ienumerable-and-value-types/5413878#5413878) for more on why the second `foreach` doesn't compile. – ladenedge Apr 11 '11 at 15:59
  • I'm sorry, should have explained in more detail. See the answer of Martinho Fernandes. – Polity Apr 11 '11 at 16:02