Как реализован foreach в C#? [дубликат]
этот вопрос уже есть ответ здесь:
- как работают циклы foreach в C#? 7 ответов
как именно 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>
явно, но есть publicGetEnumerator()
метод, который возвращает тип, отличный отIEnumerator
/IEnumerator<T>
. См.List<T>.GetEnumerator()
для примера-это позволяет избежать ненужного создания объекта ссылочного типа во многих случаи.
см. раздел 8.8.4 спецификации C# 4 для получения дополнительной информации.
удивлен, что точная реализация не тронута. Хотя то, что вы разместили в вопросе, является самой простой формой, полная реализация (включая удаление перечислителя, литье и т. д.) находится в 8.8.4 раздел спецификации.
теперь есть 2 сценария, где foreach
цикл можно запустить по типу:
-
если тип имеет открытый / нестатический/не универсальный / без параметров метод с именем
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(); }
-
если тип реализует
IEnumerable
здесьGetEnumerator
возвращаетIEnumerator
это имеет общественноеMoveNext
метод и публикаCurrent
собственность. но интересным случаем является то, что даже если вы реализуетеIEnumerable
явно (т. е. нет publicGetEnumerator
метод onTest
класс), вы можете иметь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
класс должен иметь publicMoveNext
метод и публика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
реализация не так проста, как кажется на поверхности) -
приоритет перечислителя-это похоже на то, если у вас есть
public GetEnumerator
метод, то это выбор по умолчаниюforeach
независимо от того, кто реализует ее. Например:class Test : IEnumerable<int> { public SomethingEnumerator GetEnumerator() { //this one is called } IEnumerator<int> IEnumerable<int>.GetEnumerator() { } }
если у вас нет реализация (т. е. только явная реализация), то приоритет идет как
IEnumerator<T>
>IEnumerator
. -
существует оператор 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# или, другими словами, компилятор позволяет это, если существует явное приведение между двумя типами. В противном случае компилятор мешает. Фактическое приведение выполняется во время выполнения, которое может или не может завершиться неудачей.