Во что раскрывается foreach в 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>) уже обеспечено.

Ответ 18+ 🔞

Давай разберём эту штуку с foreach, а то некоторые думают, что это какая-то магия, а там, блядь, под капотом всё просто и прозаично, как гвоздь в сапоге.

Вот смотри, пишешь ты красивый код:

foreach (string element in myList)
{
    Process(element);
}

А компилятор смотрит на это и такой: "Ага, щас мы это развернём, дружок". И делает он примерно вот такую подлянку:

IEnumerator<string> enumerator = myList.GetEnumerator();
try
{
    while (enumerator.MoveNext())
    {
        string element = enumerator.Current;
        Process(element);
    }
}
finally
{
    (enumerator as IDisposable)?.Dispose();
}

Вот и вся магия, ёпта. Никакого волшебства — просто компилятор за тебя эту простыню кода написал, чтобы ты руки не уставал.

А теперь, почему это важно, а не просто хуйня какая-то:

  1. Скорость, мать её. Если у тебя массив (T[]), то компилятор — не дурак — он это видит и вместо всей этой возни с IEnumerator делает обычный, быстрый, как пуля, цикл for. Потому что зачем огород городить, если можно в лоб пройтись? А вот для List<T> или своей коллекции — уже будет этот вариант с перечислителем.

  2. Не лезь, сука, в чужой монастырь. Ты не можешь внутри foreach взять и туда что-то добавить или удалить (myList.Add(item)). Почему? Да потому что у этого самого enumerator своё представление о жизни коллекции. Ты её изменишь — а он нихуя не знает, состояние его сломается, и он тебе выкинет InvalidOperationException. Вот и сиди ровно, пока цикл не кончится.

  3. Убрать за собой — святое дело. Этот блок finally — он не просто так. Он гарантирует, что перечислитель, если он, например, файл читал или ещё какую хрень с ресурсами делал, будет нормально закрыт и почищен. Даже если твой метод Process(element) внезапно решит, что на сегодня всё, и кинет исключение. Без этого finally могли бы быть утечки, а это, блядь, не комильфо.

Короче, вывод простой, как три копейки:
Когда ты юзаешь foreach, ты по сути доверяешь коллекции, что у неё нормально реализованы интерфейсы IEnumerable и IDisposable. В стандартных штуках типа List<T> или Dictionary — всё уже сделано, не парься. Но если вдруг начнёшь свои коллекции городить — помни, что там под капотом происходит, а то на ровном месте ошибку схватишь и будешь думать, что мир несправедлив.