Как foreach реализован в C#?

Как именно foreach реализовано в C#?

Я представляю, что часть этого выглядит как:

var enumerator = TInput.GetEnumerator();
while(enumerator.MoveNext())
{
  // do some stuff here
}

Однако я не уверен, что на самом деле происходит. Какая методология используется для возвращения enumerator.Current для каждого цикла? Возвращает ли он [для каждого цикла] или требует анонимную функцию или что-то для выполнения тела foreach?

2 ответа

Решение

Он не использует анонимную функцию, нет. По сути, компилятор преобразует код во что-то в целом эквивалентное циклу while, который вы здесь показали.

foreach это не вызов функции - он встроен в сам язык, как for петли и while петли. Нет необходимости возвращать что-либо или "принимать" какую-либо функцию.

Обратите внимание, что foreach имеет несколько интересных морщин:

  • При переборе массива (известного во время компиляции) компилятор может использовать счетчик циклов и сравнивать с длиной массива вместо использования IEnumerator
  • foreach избавится от итератора в конце; это просто для IEnumerator<T> который расширяется IDisposable, но, как IEnumerator нет, компилятор вставляет проверку для проверки во время выполнения, реализует ли итератор IDisposable
  • Вы можете перебирать типы, которые не реализуют IEnumerable или же IEnumerable<T>, пока у вас есть применимый GetEnumerator() метод, который возвращает тип с подходящим Current а также MoveNext() члены. Как отмечено в комментариях, тип также может реализовывать IEnumerable или же IEnumerable<T> явно, но есть публичный GetEnumerator() метод, который возвращает тип, отличный от IEnumerator / IEnumerator<T>, Увидеть List<T>.GetEnumerator() для примера - это позволяет избежать ненужного создания объекта ссылочного типа во многих случаях.

См. Раздел 8.8.4 спецификации C# 4 для получения дополнительной информации.

Удивил, точная реализация не коснулась. Хотя то, что вы разместили в вопросе, является самой простой формой, полная реализация (включая удаление перечислителя, приведение и т. Д.) Приведена в разделе 8.8.4 спецификации.

Сейчас есть 2 сценария, где foreach цикл может быть запущен по типу:

  1. Если у типа есть открытый / нестатический / неуниверсальный / беспараметрический метод с именем GetEnumerator который возвращает то, что имеет общественное MoveNext метод и общественность Current имущество. Как отметил г-н Эрик Липперт в этой статье блога, это было разработано так, чтобы учесть предварительную эпоху как для проблем безопасности типов, так и для проблем, связанных с боксом, в случае типов значений. Обратите внимание, что это случай утки. Например, это работает:

    class Test
    {
        public SomethingEnumerator GetEnumerator()
        {
    
        }
    }
    
    class SomethingEnumerator
    {
        public Something Current //could return anything
        {
            get { return ... }
        }
    
        public bool MoveNext()
        {
    
        }
    }
    
    //now you can call
    foreach (Something thing in new Test()) //type safe
    {
    
    }
    

    Затем он переводится компилятором в:

    E enumerator = (collection).GetEnumerator();
    try {
       ElementType element; //pre C# 5
       while (enumerator.MoveNext()) {
          ElementType element; //post C# 5
          element = (ElementType)enumerator.Current;
          statement;
       }
    }
    finally {
       IDisposable disposable = enumerator as System.IDisposable;
       if (disposable != null) disposable.Dispose();
    }
    
  2. Если тип реализует IEnumerable где GetEnumerator возвращается IEnumerator это имеет общественность MoveNext метод и общественность Current имущество. Но интересный случай суб это то, что даже если вы реализуете IEnumerable явно (т.е. нет публичного GetEnumerator метод на Test класс), вы можете иметь foreach,

    class Test : IEnumerable
    {
        IEnumerator IEnumerable.GetEnumerator()
        {
    
        }
    }
    

    Это потому что в этом случае foreach реализуется как (при условии, что нет других публичных GetEnumerator метод в классе):

    IEnumerator enumerator = ((IEnumerable)(collection)).GetEnumerator();
    try {
        ElementType element; //pre C# 5
        while (enumerator.MoveNext()) {
            ElementType element; //post C# 5
            element = (ElementType)enumerator.Current;
            statement;
       }
    }
    finally {
        IDisposable disposable = enumerator as System.IDisposable;
        if (disposable != null) disposable.Dispose();
    }
    

    Если тип реализует IEnumerable<T> явно тогда foreach преобразуется в (при условии, что нет других публичных GetEnumerator метод в классе):

    IEnumerator<T> enumerator = ((IEnumerable<T>)(collection)).GetEnumerator();
    try {
        ElementType element; //pre C# 5
        while (enumerator.MoveNext()) {
            ElementType element; //post C# 5
            element = (ElementType)enumerator.Current; //Current is `T` which is cast
            statement;
       }
    }
    finally {
        enumerator.Dispose(); //Enumerator<T> implements IDisposable
    }
    

Несколько интересных вещей, на которые стоит обратить внимание:

  1. В обоих вышеупомянутых случаях Enumerator класс должен быть публичным MoveNext метод и общественность Current имущество. Другими словами, если вы реализуете IEnumerator Интерфейс должен быть реализован неявно. Например, foreach не будет работать для этого перечислителя:

    public class MyEnumerator : IEnumerator
    {
        void IEnumerator.Reset()
        {
            throw new NotImplementedException();
        }
    
        object IEnumerator.Current
        {
            get { throw new NotImplementedException(); }
        }
    
        bool IEnumerator.MoveNext()
        {
            throw new NotImplementedException();
        }
    }
    

    (Спасибо, Рой Намир, за то, что указал на это. foreach реализация не так проста, как кажется на первый взгляд)

  2. Приоритет перечислителя - похоже, если у вас есть public GetEnumerator метод, то это выбор по умолчанию foreach независимо от того, кто осуществляет это. Например:

    class Test : IEnumerable<int>
    {
        public SomethingEnumerator GetEnumerator()
        {
            //this one is called
        }
    
        IEnumerator<int> IEnumerable<int>.GetEnumerator()
        {
    
        }
    }
    

    Если у вас нет публичной реализации (то есть только явной реализации), то приоритет имеет вид IEnumerator<T> > IEnumerator,

  3. Существует оператор приведения, участвующий в реализации foreach где элемент коллекции приводится к типу (указанному в foreach петля сама). Что означает, даже если вы написали SomethingEnumerator как это:

    class SomethingEnumerator
    {
        public object Current //returns object this time
        {
            get { return ... }
        }
    
        public bool MoveNext()
        {
    
        }
    }
    

    Вы могли бы написать:

    foreach (Something thing in new Test())
    {
    
    }
    

    Так как Something совместим ли тип с object в соответствии с правилами C# или другими словами, компилятор разрешает это, если существует явное приведение между двумя типами. В противном случае компилятор предотвращает это. Фактическое приведение выполняется во время выполнения, которое может или не может потерпеть неудачу.

Другие вопросы по тегам