Как используется foreach в С#?

Как точно реализовано foreach в С#?

Я предполагаю, что его часть выглядит следующим образом:

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

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

Ответы

Ответ 1

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

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

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

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

Дополнительную информацию см. в разделе 8.8.4 спецификации С# 4.

Ответ 2

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

Теперь существует 2 сценария, в которых цикл foreach может быть запущен для типа:

  • Если тип имеет открытый/нестатический/не общий/без параметров метод с именем 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();
    }
    
  • Если тип реализует 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
    }
    

Несколько интересных замечаний:

  • В обоих случаях класс 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();
        }
    }
    

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

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

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

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

  • В реализации 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, идя по правилам С#, или, другими словами, компилятор разрешает его, если между этими двумя типами существует явное литье. В противном случае компилятор предотвратит это. Фактический отбор выполняется во время выполнения, которое может или не может потерпеть неудачу.