Циклы в C#: for, while, do-while и foreach с примерами

Циклы в C#: for, while, do-while и foreach с примерами Полезное

Цикл в C# — это конструкция, которая повторяет блок кода, пока выполняется условие или пока не перебраны все элементы коллекции. В C# четыре оператора цикла: for (со счетчиком), while (с предусловием), do-while (с постусловием) и foreach (перебор коллекции, у него есть асинхронная форма await foreach с C# 8). Управлять ходом цикла помогают break и continue.

Ниже — один рабочий пример со всеми четырьмя циклами, таблица выбора, разбор break/continue и типичные ошибки с настоящим выводом компилятора и среды. Все примеры проверены на .NET SDK 10.0.401 (C# 14) 24 сентября 2026 года.

Четыре цикла на одном примере

Код ниже можно сохранить в файл loops.cs и запустить командой dotnet run loops.cs (в .NET 10 файл без проекта запускается напрямую). В обычном консольном проекте тот же код работает в Program.cs.

int[] prices = { 120, 45, 300, 80 };

// for: нужен индекс или шаг
int total = 0;
for (int i = 0; i < prices.Length; i++)
{
    total += prices[i];
}
Console.WriteLine($"for: сумма = {total}");

// foreach: нужны только элементы
int max = int.MinValue;
foreach (int p in prices)
{
    if (p > max) max = p;
}
Console.WriteLine($"foreach: максимум = {max}");

// while: число повторов заранее неизвестно
int balance = 500;
int months = 0;
while (balance >= 120)
{
    balance -= 120;
    months++;
}
Console.WriteLine($"while: подписка на {months} мес., остаток {balance}");

// do-while: тело выполнится хотя бы один раз
string[] inputs = { "abc", "", "42" }; // имитация ввода пользователя
int step = 0;
int number;
do
{
    Console.WriteLine($"do-while: ввод \"{inputs[step]}\"");
    step++;
} while (!int.TryParse(inputs[step - 1], out number));
Console.WriteLine($"do-while: число {number} получено с попытки {step}");

Вывод:

for: сумма = 545
foreach: максимум = 300
while: подписка на 4 мес., остаток 20
do-while: ввод "abc"
do-while: ввод ""
do-while: ввод "42"
do-while: число 42 получено с попытки 3

Как устроен каждый цикл

for — цикл со счетчиком

Заголовок for (инициализация; условие; итерация) состоит из трех частей. Порядок такой: один раз выполняется инициализация (int i = 0), затем перед каждым проходом проверяется условие (i < prices.Length), если оно истинно — выполняется тело, после тела — итерация (i++), и снова проверка условия.

Прогон руками для массива из 4 элементов: i = 0, 1, 2, 3 — условие истинно, тело выполняется; при i = 4 условие ложно, цикл завершается. Переменная i объявлена в заголовке и после цикла недоступна.

Все три части необязательны: for (;;) — бесконечный цикл, который завершают через break или return. Шаг может быть любым: i += 2 для каждого второго элемента, i-- для обратного прохода.

while — цикл с предусловием

while (условие) { ... } проверяет условие до каждого прохода. Если оно ложно с самого начала, тело не выполнится ни разу. Подходит, когда число повторов заранее неизвестно: читать, пока есть данные; уменьшать баланс, пока хватает денег.

do-while — цикл с постусловием

do { ... } while (условие); сначала выполняет тело, потом проверяет условие. Поэтому тело выполняется минимум один раз. Типичный сценарий — запросить ввод и повторять, пока он некорректен. Обратите внимание на точку с запятой после while (...) — в do-while она обязательна.

foreach — перебор коллекции

foreach (var элемент in коллекция) проходит по массиву, списку, строке (по символам) и любому типу с подходящим методом GetEnumerator() (его результат должен иметь MoveNext() и Current). Индекс здесь не нужен, выйти за границы массива нельзя. Ограничения: переменную цикла нельзя присвоить, а коллекцию нельзя менять во время перебора (оба случая разобраны ниже). Подробно устройство foreach и интерфейса IEnumerable — тема отдельного разбора, здесь достаточно правил использования.

Условие — только bool

В отличие от C и C++, в C# условие цикла обязано иметь тип bool. Код while (n), где n — целое, не компилируется:

error CS0029: Cannot implicitly convert type 'int' to 'bool'

Исправление — явное сравнение: while (n != 0). Как те же циклы выглядят в C и C++, где число в условии допустимо, разобрано в статье о циклах в C и C++.

Какой цикл выбрать

Задача Цикл Почему
Пройти все элементы, индекс не нужен foreach Короче и без риска выйти за границы
Нужен индекс, шаг, обратный порядок, соседние элементы for Счетчик под полным контролем
Повторять, пока выполняется условие, число шагов неизвестно while Проверка до первого прохода
Тело должно выполниться хотя бы раз (ввод, меню, повтор запроса) do-while Проверка после прохода
Удалять элементы списка по условию for с конца или RemoveAll foreach не разрешает менять коллекцию

Упрощение: любой цикл можно переписать через while, так что таблица говорит о читаемости и типичном риске ошибок, а не о возможностях.

break и continue

continue пропускает остаток текущего прохода и переходит к следующему. break завершает цикл целиком.

int[] data = { 4, -1, 7, 0, 9, 3 };

// continue: пропустить итерацию
int sum = 0;
foreach (int x in data)
{
    if (x <= 0) continue;   // отрицательные и ноль пропускаем
    sum += x;
}
Console.WriteLine($"сумма положительных = {sum}");

// break: выйти из цикла при первой находке
int index = -1;
for (int i = 0; i < data.Length; i++)
{
    if (data[i] > 5)
    {
        index = i;
        break;
    }
}
Console.WriteLine($"первое число больше 5 - индекс {index}");

// break во вложенном цикле выходит только из внутреннего
int[,] grid = { { 1, 2, 3 }, { 4, 99, 6 }, { 7, 8, 9 } };
Console.WriteLine($"позиция 99: {Find(grid, 99)}");

static string Find(int[,] g, int target)
{
    for (int r = 0; r < g.GetLength(0); r++)
        for (int c = 0; c < g.GetLength(1); c++)
            if (g[r, c] == target)
                return $"строка {r}, столбец {c}"; // return выходит из обоих циклов
    return "не найдено";
}

Вывод:

сумма положительных = 23
первое число больше 5 - индекс 2
позиция 99: строка 1, столбец 1

Во вложенных циклах break завершает только ближайший цикл. Чтобы выйти из обоих, удобнее всего вынести циклы в метод и использовать return, как в Find. Другие варианты — флаг, который проверяет внешний цикл, или goto на метку после циклов (работает, но читается хуже).

Типичные ошибки в циклах C

Выход за границы массива

int[] a = { 10, 20, 30 };
for (int i = 0; i <= a.Length; i++)
{
    Console.WriteLine(a[i]);
}

Результат: три числа, затем исключение на i = 3:

10
20
30
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.

Индексы массива из 3 элементов — 0, 1, 2. Исправление: условие i < a.Length или foreach, если индекс не нужен.

Изменение коллекции внутри foreach

var list = new List<int> { 1, 2, 3, 4 };
foreach (int x in list)
{
    if (x % 2 == 0) list.Remove(x);
}
Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.

Исправление — обратный for (при удалении с конца индексы еще не пройденных элементов не сдвигаются) или готовый метод:

var list = new List<int> { 1, 2, 3, 4 };
for (int i = list.Count - 1; i >= 0; i--)
{
    if (list[i] % 2 == 0) list.RemoveAt(i);
}
Console.WriteLine(string.Join(", ", list));   // 1, 3

var list2 = new List<int> { 1, 2, 3, 4 };
list2.RemoveAll(x => x % 2 == 0);
Console.WriteLine(string.Join(", ", list2));  // 1, 3

Присваивание переменной foreach

Код foreach (int x in a) { x = x * 2; } не компилируется:

error CS1656: Cannot assign to 'x' because it is a 'foreach iteration variable'

Чтобы изменить элементы массива, нужен for с индексом: a[i] = a[i] * 2.

Точка с запятой после while

int i = 0;
while (i < 3);
{
    Console.WriteLine(i);
    i++;
}

Точка с запятой становится пустым телом цикла, а блок в скобках — обычным кодом после него. Условие i < 3 никогда не меняется, программа зависает без вывода. Компилятор предупреждает, но сборку не останавливает:

warning CS0642: Possible mistaken empty statement

Исправление — убрать ; после while (i < 3). Предупреждения при сборке стоит читать, а не пропускать.

Беззнаковый счетчик в обратном цикле

int n = 0;
for (uint i = 3; i >= 0; i--)
{
    Console.WriteLine(i);
    if (++n == 6) break; // страховка, иначе цикл бесконечный
}
3
2
1
0
4294967295
4294967294

Тип uint не бывает отрицательным: после 0 значение переходит к максимуму, и i >= 0 истинно всегда. В этом прогоне компилятор предупреждения не выдал. Исправление — счетчик типа int.

Сравнение дробного счетчика на равенство

Цикл for (double x = 0; x != 1.0; x += 0.1) не останавливается: из-за двоичного представления после десяти шагов x не равен ровно 1.0. В прогоне со страховкой на 20 шагов цикл проскочил единицу и дошел до x = 2.0000000000000004. Исправление — считать шаги целым счетчиком (for (int t = 0; t <= 10; t++), а x = t / 10.0) или сравнивать через <.

Лямбда захватывает переменную for

var fromFor = new List<Action>();
for (int i = 0; i < 3; i++)
    fromFor.Add(() => Console.Write(i + " "));
foreach (var f in fromFor) f();
Console.WriteLine("<- for");

var fromForeach = new List<Action>();
foreach (int n in new[] { 0, 1, 2 })
    fromForeach.Add(() => Console.Write(n + " "));
foreach (var f in fromForeach) f();
Console.WriteLine("<- foreach");
3 3 3 <- for
0 1 2 <- foreach

В for переменная i одна на весь цикл, и все лямбды видят ее последнее значение 3. В foreach с C# 5 переменная создается заново на каждом проходе. Исправление для for — копия внутри тела: int copy = i; и захват copy.

Если цикл работает не так

  • Программа зависла без вывода — проверьте ; после while (...), меняется ли переменная условия в теле, нет ли uint в обратном цикле или сравнения double через !=.
  • IndexOutOfRangeException — условие <= вместо < или обращение к a[i + 1] на последнем шаге.
  • InvalidOperationException в foreach — коллекция меняется во время перебора, нужен обратный for или RemoveAll.
  • Цикл не выполнился ни разу — условие while/for ложно с самого начала; если тело нужно хотя бы один раз, используйте do-while.

Выводы

  • В C# четыре цикла: for со счетчиком, while с предусловием, do-while с постусловием (тело выполняется минимум раз) и foreach для перебора коллекций.
  • foreach — выбор по умолчанию для обхода всех элементов; for — когда нужны индекс, шаг или изменение коллекции.
  • break выходит только из ближайшего цикла; из вложенных удобнее выходить через return из вынесенного метода.
  • Условие цикла в C# — только bool, а переменную foreach и саму коллекцию во время перебора менять нельзя.
  • Частые ошибки: <= вместо <, ; после while, uint в обратном цикле, != для double, захват переменной for в лямбде.

Где применяется / связь с практикой

Циклы в C# встречаются в любом типе проекта: обработка данных и файлов, игровой цикл в Unity, повторные попытки запроса к API, обход результатов из базы. Уверенное владение циклами и коллекциями — база, на которой строятся LINQ, асинхронный код и работа с потоками данных.

Освойте тему на практике

Системно пройти синтаксис C#, коллекции и ООП с практикой и ревью кода можно на курсе по основам C#. Чтобы посмотреть формат обучения, подойдут бесплатные открытые уроки Otus.

FAQ

Что быстрее: for или foreach?
Для массивов компилятор обычно превращает foreach в проход по индексу, и разница незаметна. Для List<T> и других коллекций она зависит от типа и версии среды, поэтому решать стоит по читаемости, а скорость измерять на своем коде (например, BenchmarkDotNet).

Можно ли в foreach узнать номер элемента?
Отдельной переменной-индекса у foreach нет. Можно завести счетчик до цикла и увеличивать его в теле, а в .NET 9 и новее — перебирать list.Index(), который возвращает пары (индекс, элемент).

Чем цикл отличается от рекурсии?
Цикл повторяет код внутри одного вызова метода, рекурсия — через повторные вызовы метода самого себя. Для простого перебора цикл проще и не расходует стек вызовов.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)