Обойти коллекцию целиком – самая частая задача с циклами. Для неё есть отдельная конструкция, которая избавляет от счётчика, а вместе с ним и от ошибок с индексами.
foreach
string[] names = { "Аня", "Борис", "Вера" };
foreach (string name in names)
{
Console.WriteLine(name);
}
Читается почти по-русски: «для каждого имени в списке имён». Индексов нет, границ нет, выйти за пределы массива невозможно в принципе.
Тот же обход через for выглядит так:
for (int i = 0; i < names.Length; i++)
{
Console.WriteLine(names[i]);
}
Три места, где можно ошибиться (начальное значение, условие, индексация), против ни одного.
Поэтому правило простое: нужны все элементы подряд – берите foreach.
Тип элемента обычно пишут как var – он и так известен из коллекции:
foreach (var name in names)
Чего foreach не умеет
Не даёт номер элемента. Если нужен индекс, придётся считать самому или брать for:
int index = 0;
foreach (var name in names)
{
Console.WriteLine($"{index}. {name}");
index++;
}
Не позволяет менять переменную цикла. Такая строка не соберётся:
foreach (var name in names)
{
name = name.ToUpper(); // ошибка компиляции
}
Запрет осмысленный: присваивание изменило бы только локальную переменную, а не элемент коллекции. Компилятор не даёт написать код, который выглядит как изменение массива, но им не является.
Менять саму коллекцию во время foreach тоже нельзя – добавление или удаление элемента прерывает обход исключением InvalidOperationException. Если нужно удалить часть элементов, собирайте их в отдельный список, а удаляйте после цикла.
break: выйти из цикла
foreach (var name in names)
{
if (name == "Борис")
{
Console.WriteLine("Нашли");
break;
}
}
break немедленно заканчивает цикл – оставшиеся элементы не проверяются. Это стандартный способ
поиска: нашли – дальше искать незачем.
Из вложенных циклов break выходит только из своего, внутреннего. Чтобы прервать оба, обычно
заводят флаг или выносят поиск в отдельный метод с return – о методах речь в следующем модуле.
continue: пропустить повторение
foreach (var number in numbers)
{
if (number % 2 != 0)
{
continue;
}
Console.WriteLine(number);
}
continue завершает текущий проход и переходит к следующему элементу. Всё, что ниже, для этого
элемента не выполняется.
Тот же результат даёт обычный if без continue, и для одного условия так даже короче. continue
окупается, когда отсеивающих проверок несколько: без него тело уползает вправо на три уровня
вложенности.
foreach (var order in orders)
{
if (order is null) continue;
if (order.IsCancelled) continue;
if (order.Total <= 0) continue;
Process(order); // здесь остались только годные заказы
}
Такой приём называют ранним выходом. Читателю не нужно держать в голове три вложенных условия: он видит список причин пропустить элемент, а ниже – работу с тем, что прошло отбор.
Обход словаря
foreach работает с любой коллекцией, но у словаря элемент – это пара:
var ages = new Dictionary<string, int>
{
["Аня"] = 19,
["Борис"] = 25
};
foreach (var pair in ages)
{
Console.WriteLine($"{pair.Key}: {pair.Value}");
}
Пару удобно сразу разложить на две переменные:
foreach (var (name, age) in ages)
{
Console.WriteLine($"{name}: {age}");
}
Подробно про словари – в модуле про коллекции; здесь важно только то, что foreach обходит их
так же, как массивы.
goto: почему его здесь нет
В C# есть оператор безусловного перехода goto, и он даже работает. Пользоваться им не нужно.
Любой цикл с goto записывается через for, while, break и continue – и в этом виде он
читается, потому что границы повторяемого куска видны глазами. С goto границ нет: чтобы понять
код, приходится прослеживать переходы вручную.
Единственное место, где goto в C# встречается на практике, – переход между ветками switch
(goto case 2;). Но и там обычно проще перестроить условия.