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

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

Ответ

Компилятор C# преобразует foreach в низкоуровневый код, используя интерфейсы IEnumerable/IEnumerator. Конкретная реализация зависит от типа коллекции.

1. Для коллекций, реализующих IEnumerable<T> или IEnumerable: Генерируется код, явно использующий перечислитель с гарантированным освобождением ресурсов через IDisposable.

// Исходный код:
foreach (var item in collection)
{
    Console.WriteLine(item);
}

// Примерный код после компиляции:
IEnumerator enumerator = collection.GetEnumerator();
try
{
    while (enumerator.MoveNext())
    {
        var item = enumerator.Current;
        Console.WriteLine(item); // Тело цикла
    }
}
finally
{
    if (enumerator is IDisposable disposable)
    {
        disposable.Dispose(); // Важно для освобождения ресурсов
    }
}

2. Для массивов (тип T[]): Компилятор генерирует более эффективный цикл for с доступом по индексу, минуя накладные расходы на создание перечислителя.

// Исходный код:
foreach (var item in array)
{
    Console.WriteLine(item);
}

// Примерный код после компиляции:
for (int i = 0; i < array.Length; i++)
{
    var item = array[i];
    Console.WriteLine(item); // Тело цикла
}

Ключевые особенности:

  • Гарантия очистки: Используется блок try/finally для вызова Dispose() у перечислителя, что критично для таких ресурсов, как IEnumerator, возвращаемый при работе с файлами или сетевыми потоками.
  • Оптимизация для массивов: Прямой доступ по индексу делает итерацию по массивам максимально быстрой.
  • Неизменяемость коллекции: Во время выполнения foreach исходную коллекцию изменять нельзя, иначе будет выброшено исключение InvalidOperationException.
  • Требуемый интерфейс: Цикл работает только с типами, реализующими IEnumerable или являющимися массивами.