Как 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
цикл может быть запущен по типу:
Если у типа есть открытый / нестатический / неуниверсальный / беспараметрический метод с именем
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(); } }
(Спасибо, Рой Намир, за то, что указал на это.
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
в соответствии с правилами C# или другими словами, компилятор разрешает это, если существует явное приведение между двумя типами. В противном случае компилятор предотвращает это. Фактическое приведение выполняется во время выполнения, которое может или не может потерпеть неудачу.