Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is the C# compiler happy with double IEnumerable<T> and foreach T?

I know this code does not work (and have no problems writing it in a way that will work). I was wondering how the compiler can build with out any errors. And you get run time errors if you where to run it? ( assuming data was not null )

using System;
using System.Collections.Generic;

public class Class1
{
    public void Main()
    {
        IEnumerable<IEnumerable<Foo>> data = null;

        foreach(Foo foo in data){
            foo.Bar();
        }
    }

}

public class Foo {
    public void Bar() { }
}
like image 625
lockwobr Avatar asked Nov 25 '13 22:11

lockwobr


People also ask

Why is called C?

After language 'B', Dennis Ritchie came up with another language which was based upon 'B'. As in alphabets B is followed by C and hence he called this language as 'C'.

Why are we using C?

C is a general-purpose programming language and can efficiently work on enterprise applications, games, graphics, and applications requiring calculations, etc. C language has a rich library which provides a number of built-in functions. It also offers dynamic memory allocation.

Why is C not A or B?

Because C comes after B The reason why the language was named “C” by its creator was that it came after B language. Back then, Bell Labs already had a programming language called “B” at their disposal.


1 Answers

This is because foreach does not do compile time checking in your specific case. If you built working code you get a InvalidCastException at run-time.

using System.Collections.Generic;

public class Test
{
    internal class Program
    {
        public static void Main()
        {
            var item = new Foo();
            var inner = new List<Foo>();
            var outer = new List<List<Foo>>();

            inner.Add(item);
            outer.Add(inner);

            IEnumerable<IEnumerable<Foo>> data = outer;

            foreach (Foo foo in data)
            {
                foo.Bar();
            }
        }

    }


    public class Foo
    {
        public void Bar()
        {
        }
    }
}

doing foreach (Foo foo in data) is equivalent to calling

IEnumerator enumerator = ((IEnumerable)data).GetEnumerator();
Foo foo; //declared here in C# 4 and older
while(enumerator.MoveNext())
{
    //Foo foo; //declared here in C# 5 and newer

    foo = (Foo)enumerator.Current; //Here is the run time error in your code.

    //The code inside the foreach loop.
    {
        foo.Bar();
    }
}

So you see it does not care what type you passed in, as long as the foo = (Foo)enumerator.Current; call succeeds.


The reason it does not throw any compile time errors is IEnumerable<T> is covariant. That means I am allowed to pass any class that is based on Foo or more derived from Foo. So if I could potentially make a 2nd class that inherits from Foo that would also support IEnumerable<Foo> and have my list contain that instead it would cause the cast to fail.

//This code compiles fine in .NET 4.5 and runs without throwing any errors.
internal class Program
{
    public static void Main()
    {
        var item = new Baz();
        var inner = new List<Baz>();
        inner.Add(item);

        IEnumerable<IEnumerable<Foo>> data = inner;

        foreach (Foo foo in data)
        {
            foo.Bar();
        }
    }
}

public class Foo
{
    public void Bar()
    {
    }
}

public class Baz : Foo, IEnumerable<Foo>
{
    IEnumerator IEnumerable.GetEnumerator()
    {
        throw new NotImplementedException();
    }

    IEnumerator<Foo> IEnumerable<Foo>.GetEnumerator()
    {
        throw new NotImplementedException();
    }
}

However if you mark Foo as sealed the compiler now knows that no more derived classes could exist and then will throw the compiler error

like image 113
Scott Chamberlain Avatar answered Oct 06 '22 00:10

Scott Chamberlain