Во что раскрывается foreach в C#?

«Во что раскрывается foreach в C#?» — вопрос из категории C# Core, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Конструкция foreach является синтаксическим сахаром. Компилятор C# раскрывает её в явный код, использующий интерфейсы IEnumerable/IEnumerator. Это не просто макроподстановка, а преобразование, учитывающее освобождение ресурсов (IDisposable).

Базовое раскрытие для коллекций:

// Исходный код разработчика:
foreach (string element in myList)
{
    Process(element);
}

// Эквивалентный код, генерируемый компилятором:
IEnumerator<string> enumerator = myList.GetEnumerator();
try
{
    // Цикл перемещается по коллекции, пока есть элементы.
    while (enumerator.MoveNext())
    {
        string element = enumerator.Current; // Получение текущего элемента
        Process(element); // Тело оригинального цикла
    }
}
finally
{
    // Обязательная очистка. Проверка на IDisposable безопасна даже для устаревших коллекций.
    (enumerator as IDisposable)?.Dispose();
}

Почему это важно понимать:

  1. Производительность: Для массивов (T[]) компилятор генерирует оптимизированный цикл for, что быстрее, чем использование IEnumerator.
  2. Поведение при ошибках: Вы не можете модифицировать коллекцию внутри foreach (например, myList.Add(item)), потому что это нарушит состояние внутреннего перечислителя (enumerator), что приведёт к InvalidOperationException.
  3. Управление ресурсами: Блок finally гарантирует, что перечислитель будет корректно освобождён, даже если в теле цикла произойдёт исключение. Это особенно важно для перечислителей, работающих с неуправляемыми ресурсами.

Практический вывод: Используя foreach, вы неявно полагаетесь на корректную реализацию IEnumerable и IDisposable в используемой коллекции, что в стандартных типах (List<T>, Dictionary<TKey,TValue>) уже обеспечено.