Menu

Цикл foreach в C#: массивы, List, Dictionary, индекс и ошибки

Как работает foreach в C#: обход массивов, списков, строк и словарей (KeyValuePair и деконструкция), получение индекса, почему переменная цикла доступна только для чтения, InvalidOperationException «Collection was modified» и как его исправить, что нужно типу для использования в foreach.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

foreach выполняет своё тело по одному разу для каждого элемента коллекции, по порядку, без индекса, которым нужно управлять:

Вывод:

Welcome, Ava
Welcome, Noah
Welcome, Mia
Total: 255
[C][#][7]

Цикл одинаково работает с массивом, List<T>, строкой (тип элемента char), HashSet<T>, запросом LINQ и любой другой последовательностью. В качестве объявленного типа можно указать var, что распространено, когда тип элемента длинный: foreach (var order in pendingOrders).

Что foreach делает за кулисами

foreach запрашивает у коллекции перечислитель и проходит по нему. Для List<int> компилятор превращает цикл примерно в следующее:

List<int>.Enumerator e = scores.GetEnumerator();
try
{
    while (e.MoveNext())
    {
        int score = e.Current;
        // loop body
    }
}
finally
{
    e.Dispose();
}

Отсюда три следствия. Подходит любой тип с открытым методом GetEnumerator(), который обычно появляется благодаря реализации IEnumerable<T>. Перечислитель освобождается, даже когда тело выбрасывает исключение или выполняет break. А переменная элемента на каждом проходе это новая копия e.Current, поэтому присваивание ей не может изменить коллекцию.

Тип без GetEnumerator использовать нельзя:

error CS1579: foreach statement cannot operate on variables of type 'Order' because 'Order' does not contain a public instance or extension definition for 'GetEnumerator'

Реализуйте для типа IEnumerable<T> или перебирайте коллекцию, которую он содержит (order.Lines вместо order). Собственные последовательности можно писать через yield return, это описано на странице об IEnumerable и yield.

Перебор словаря

Элементы словаря это значения KeyValuePair<TKey, TValue> со свойствами Key и Value:

Вывод:

apples: 12
pears: 0
kiwis: 30
<apples><pears><kiwis>
Units in stock: 42
 30 kiwis
 12 apples
  0 pears

Dictionary не обещает никакого порядка перебора. На практике он следует порядку вставки, пока вы не удалите элементы, но код, который зависит от порядка, должен сортировать явно, как это делает последний цикл с OrderByDescending, или использовать SortedDictionary.

В .NET Core 2.0 и новее (и в любом современном .NET) у KeyValuePair есть метод Deconstruct, поэтому пару можно разделить на две переменные прямо в заголовке цикла:

// modern .NET: KeyValuePair deconstruction
foreach (var (fruit, count) in stock)
{
    Console.WriteLine($"{fruit}: {count}");
}

Это самая короткая форма, и именно её вы увидите в современном коде. Она не компилируется в .NET Framework, где у KeyValuePair нет Deconstruct; там используйте item.Key и item.Value.

Получение индекса

Собственного индекса у foreach нет. Два обычных способа получить его: счётчик, который вы ведёте сами, или перегрузка Select из LINQ, которая передаёт индекс вместе с каждым элементом:

Вывод:

1. Kenji
2. Sofia
3. Omar
index 0: Kenji
index 1: Sofia
index 2: Omar

Счётчик самый дешёвый и понятный. Если вы используете индекс, чтобы читать podium[i - 1] или записывать podium[i] = ..., переходите на цикл for, который создан именно для этого.

Переменная цикла доступна только для чтения

Присваивать значение переменной итерации нельзя:

foreach (string name in names)
{
    name = name.Trim();
    // error CS1656: Cannot assign to 'name' because it is a 'foreach iteration variable'
}

Даже если бы это было разрешено, изменилась бы только локальная копия. Чтобы преобразовать каждый элемент, соберите новую последовательность (names.Select(n => n.Trim()).ToArray()) или используйте цикл for, который записывает names[i] = names[i].Trim().

Что можно сделать, зависит от того, является ли элемент классом или структурой. Для класса переменная хранит ссылку, поэтому изменение свойства меняет объект в коллекции:

Вывод:

Order 1: shipped
Order 2: shipped

Если бы Order был struct, то же присваивание завершилось бы ошибкой CS1654 (Cannot modify members of 'order' because it is a 'foreach iteration variable'), потому что переменная это копия значения, а не ссылка на него.

Изменение коллекции: InvalidOperationException

Добавление или удаление элементов List или Dictionary, пока foreach их перебирает, ломает перечислитель. Следующий MoveNext выбрасывает InvalidOperationException с сообщением «Collection was modified; enumeration operation may not execute.»:

Вывод:

InvalidOperationException: collection was modified
milk, bread
milk, bread

Способы исправления, от самого простого к самому гибкому:

  • RemoveAll(predicate) у List<T> удаляет все совпадения за один проход.
  • Перебор копии через ToList() или ToArray(), после чего оригинал можно свободно менять.
  • Сначала собрать, потом применить: во время цикла складывайте ключи для удаления в отдельный список, а удаляйте их после. Это обычный приём для Dictionary.
  • Обратный цикл for с RemoveAt(i).

У Dictionary есть одно исключение, которое стоит знать. Начиная с .NET Core 3.0 перезапись значения существующего ключа (stock[key] = 0) или удаление элементов во время foreach по словарю больше не выбрасывает исключение, а добавление нового ключа по-прежнему выбрасывает. В .NET Framework все три действия выбрасывают исключение, поэтому вариант со сбором ключей работает везде.

break, continue и коллекции null

break и continue ведут себя так же, как в любом другом цикле:

Вывод:

reading 21
reading 23
no tags, no crash

Перебор коллекции null выбрасывает NullReferenceException при вызове GetEnumerator. ?? Enumerable.Empty<T>() превращает последовательность, которая может быть null, в пустую; а ещё лучше возвращать из собственных методов пустые коллекции вместо null.

foreach и List.ForEach

У List<T> есть метод ForEach(Action<T>): names.ForEach(n => Console.WriteLine(n));. Это не то же самое, что цикл. Его тело это лямбда, поэтому break, continue и yield недоступны, а return лишь завершает вызов для текущего элемента. Он есть только у List<T>, но не у массивов или других последовательностей. Предпочитайте оператор foreach; используйте ForEach только для однострочного действия, которое у вас уже есть в виде метода.

Часто задаваемые вопросы

Как перебрать Dictionary через foreach в C#?

Каждый элемент это KeyValuePair<TKey, TValue>: foreach (var pair in prices) Console.WriteLine($"{pair.Key}: {pair.Value}");. В .NET Core 2.0 и новее пару можно деконструировать: foreach (var (name, price) in prices). Чтобы перебрать только ключи или значения, используйте prices.Keys или prices.Values.

Как получить индекс в цикле foreach в C#?

Встроенного индекса у foreach нет. Заведите счётчик (int i = 0; перед циклом, i++ в конце тела) или спроецируйте каждый элемент вместе с индексом через LINQ: foreach (var item in names.Select((name, index) => new { name, index })). Если индекс нужен, чтобы записывать обратно или сравнивать соседей, лучше подходит цикл for.

Что означает «Collection was modified; enumeration operation may not execute»?

Вы добавили или удалили элементы в List, Dictionary или другой коллекции, пока foreach её перебирал. Перечислитель обнаруживает изменение и на следующем шаге выбрасывает InvalidOperationException. Перебирайте копию (foreach (var x in list.ToList())), собирайте изменения и применяйте их после цикла, используйте list.RemoveAll(...) или обратный цикл for.

Почему нельзя присвоить значение переменной foreach в C#?

Переменная итерации доступна только для чтения: name = name.Trim(); внутри foreach (string name in names) это ошибка CS1656. Присваивание всё равно не изменило бы коллекцию. Чтобы заменить элементы, используйте цикл for с индексом или соберите новую коллекцию через Select. Изменять свойство объекта класса (order.Status = ...) можно.

Можно ли выйти из цикла foreach в C#?

Да. break сразу завершает цикл, а continue переходит к следующему элементу, точно так же как в for и while. return выходит из всего метода. Метод List<T>.ForEach устроен иначе: его тело это лямбда, поэтому break и continue там недоступны.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ