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