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