Подтвердить что ты не робот

Почему компилятор С# счастлив с двойным IEnumerable <T> и foreach T?

Я знаю, что этот код не работает (и у него нет проблем с написанием его таким образом, который будет работать). Мне было интересно, как компилятор может строить без каких-либо ошибок. И вы получаете ошибки времени выполнения, если вы его запускаете? (если данные не были нулевыми)

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() { }
}
4b9b3361

Ответ 1

Это связано с тем, что foreach не выполняет проверку времени компиляции в вашем конкретном случае. Если вы создали рабочий код, вы получите a InvalidCastException во время выполнения.

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()
        {
        }
    }
}

Выполнение foreach (Foo foo in data) эквивалентно вызову

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();
    }
}

Итак, вы видите, что все равно, какой тип вы передали, если вызов foo = (Foo)enumerator.Current; преуспевает.


Причина, по которой он не выбрасывает ошибки времени компиляции, IEnumerable<T> covariant. Это означает, что мне разрешено передавать любой класс, основанный на Foo или больше, полученный из Foo. Поэтому, если я могу потенциально сделать 2-й класс, который наследует от Foo, который также поддерживает IEnumerable<Foo> и содержит мой список, вместо этого это приведет к сбою приведения.

//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();
    }
}

Однако, если вы помечаете Foo как sealed, теперь компилятор знает, что не может существовать больше производных классов, а затем будет вызывать ошибку компилятора

Ответ 2

Поскольку вы явно указали тип, каждая итерация foreach будет пытаться выполнить (во время выполнения) текущий элемент Foo. Это ничем не отличается от написания:

IEnumerable<IEnumerable<Foo>> data = null;
foreach (object item in data)
{
    Foo foo = (Foo)item;
    foo.Bar();
}

Или более непосредственно, это:

IEnumerable<Foo> data = null;
Foo foo = (Foo)data;

Компилятор не жалуется по той же причине, что он не жалуется на приведение к интерфейсу или из него: он не может доказать, что приведение недействительно (см. здесь для примера того, почему).

Обратите внимание, что, напротив, если вы работаете с конкретным классом вместо интерфейса IEnumerable, тогда вы получите ошибку времени компиляции. Например:

IEnumerable<List<Foo>> data = null;

foreach(Foo foo in data){  // compile-time error here: "cannot convert List<Foo> to Foo"
    foo.Bar();
}

Ответ 4

Вы можете сделать ошибку времени компиляции, отметив свой класс Foo как sealed:

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

В противном случае компилятор не может быть уверен в возможности преобразования.