Как реализован 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() членов. Как отмечается в комментариях, тип can и реализовать IEnumerable или IEnumerable<T> явно, но есть public GetEnumerator() метод, который возвращает тип, отличный от IEnumerator/IEnumerator<T>. См.List<T>.GetEnumerator() для примера-это позволяет избежать ненужного создания объекта ссылочного типа во многих случаи.

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


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

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

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

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