$ sudo teach IT
Модуль 2 · Логика программы · Урок 2.3

Циклы

Вы научитесь повторять действия через loop, while и for и обходить массив трат одной короткой записью.

Теория + практика~25 минутНовичок

Функции у нас уже есть, ветвления тоже, но функция total всё ещё складывает три траты вручную: expenses[0] + expenses[1] + expenses[2]. Появится четвёртая трата, или сотая, и такую запись придётся переписывать. Не хватает умения говорить программе «повтори это столько раз, сколько нужно», и именно этим мы займёмся.

loop: крутимся, пока не скажем «стоп»

Представьте проигрыватель, у которого включён режим «повтор». Песня доигрывает и тут же начинается снова, и так до бесконечности, пока вы сами не нажмёте «стоп». Самый простой цикл в Rust работает ровно так.

Официально это цикл (loop): блок кода, который выполняется снова и снова. Начинается он со слова loop, а выйти из него можно только командой break (по-английски «сломать, прервать»). Смотрим пример.

Rust · main.rs
fn main() {
    let mut n = 0;
    loop {
        n += 1;
        if n == 2 {
            continue;
        }
        println!("шаг {n}");
        if n == 4 {
            break;
        }
    }
}

Разбор по строкам

  • let mut n = 0; — счётчик. Он должен меняться, поэтому mut.
  • loop { ... } — всё внутри фигурных скобок будет выполняться по кругу.
  • n += 1; — на каждом круге прибавляем единицу, то есть считаем круги.
  • if n == 2 { continue; } — continue значит «пропусти остаток этого круга и начинай следующий». Когда n равно 2, печать ниже не выполнится.
  • println!("шаг {n}"); — печатает номер шага.
  • if n == 4 { break; } — на четвёртом круге выходим из цикла, и программа идёт дальше.
Вывод
шаг 1
шаг 3
шаг 4

Шага 2 в выводе нет: на этом круге сработал continue. Запомнить удобно так: break выходит из цикла совсем, а continue только перескакивает на следующий круг.

break может вернуть значение

У loop есть особенность, которой нет у других циклов: он сам может быть выражением (как if из прошлого урока). Если после break написать значение, цикл «вернёт» его, и результат можно положить в переменную. Допустим, вы откладываете по 1000 рублей в месяц и хотите узнать, за сколько месяцев накопится хотя бы 4500.

Rust · main.rs
let mut saved = 0;
let mut months = 0;
let result = loop {
    saved += 1000;
    months += 1;
    if saved >= 4500 {
        break months;
    }
};
println!("{result}");

Разбор по строкам

  • let mut saved = 0; — сколько накопили; let mut months = 0; — сколько месяцев прошло.
  • let result = loop { ... }; — всё, что цикл вернёт через break, попадёт в result. После закрывающей скобки нужна ;, потому что это обычная инструкция let.
  • break months; — выходим из цикла и отдаём число месяцев. Выведется 5: после пятого месяца накоплено 5000.

Цикл без break не закончится

Если внутри loop нет break (или его условие никогда не выполняется), программа будет крутиться вечно. Остановить её в терминале можно сочетанием Ctrl+C.

while: пока условие верно

Иногда мы заранее не знаем, сколько кругов понадобится, но знаем условие, при котором работа продолжается. Как с чайником: «пока в чашке не налито до краёв, подливаем». В Rust это цикл while («пока»). Перед каждым кругом он проверяет условие; если оно верно (true), делает круг, если нет — цикл заканчивается.

Условие, как и в if, обязано быть типа bool: сравнение вроде tries > 0, а не просто число.

Rust · main.rs
let mut tries = 3;
while tries > 0 {
    println!("Осталось попыток: {tries}");
    tries -= 1;
}
println!("Попытки закончились");

Разбор по строкам

  • let mut tries = 3; — счётчик попыток, меняется, поэтому mut.
  • while tries > 0 { — пока попыток больше нуля, выполняем тело цикла.
  • tries -= 1; — уменьшаем счётчик на единицу. Без этой строки условие никогда не станет ложным, и цикл будет бесконечным.
  • println!("Попытки закончились"); — выполнится один раз, когда while закончит работу.
Вывод
Осталось попыток: 3
Осталось попыток: 2
Осталось попыток: 1
Попытки закончились

В while тоже работают break и continue, но break здесь ничего не возвращает: просто выходит из цикла. Значение отдаёт только loop.

for: обходим массив и диапазон

Теперь самое частое. Допустим, у вас есть список покупок, и вы идёте по нему пункт за пунктом сверху вниз. Вам не надо считать пункты и следить, где остановиться: список сам заканчивается. Для этого в Rust есть цикл for, и он работает с любым набором элементов: массивом, диапазоном и, позже, другими коллекциями. Один проход цикла называют итерацией.

По массиву

Rust · main.rs
let expenses = [350, 1200, 99];
for e in expenses {
    println!("Трата: {e} руб.");
}

Разбор по строкам

  • let expenses = [350, 1200, 99]; — уже знакомый массив из трёх трат.
  • for e in expenses { — читается «для каждого e из expenses». На первом круге в e лежит 350, на втором 1200, на третьем 99. Имя e мы придумали сами, это такая же переменная, но она существует только внутри цикла.
  • println!("Трата: {e} руб."); — печатает текущую трату. Всего будет три круга, по числу элементов.

Обратите внимание: ни счётчика, ни индексов, ни проверки «а не вышли ли мы за границу». Цикл for сам знает, когда массив кончился, поэтому выйти за его границы в такой записи нельзя.

Почему массив можно использовать и после цикла

Массивы из чисел при передаче в цикл и в функцию копируются, так же как сами числа. Поэтому одну и ту же переменную expenses можно отдать сначала в total, потом в max_expense. Почему так, подробно разберём в модуле про владение.

По диапазону

Иногда нужно просто повторить действие N раз или пройти по числам подряд. Для этого есть диапазон (range): запись начало..конец. Важная деталь: конец не входит в диапазон. Если конец нужен тоже, добавляют знак равно: начало..=конец.

ЗаписьКакие числа получатсяСколько кругов
0..30, 1, 23
1..=31, 2, 33
(0..3).rev()2, 1, 03
5..5пусто0
Rust · main.rs
let expenses = [350, 1200, 99];
for i in 0..3 {
    println!("Трата {}: {} руб.", i + 1, expenses[i]);
}
for k in (1..=3).rev() {
    println!("{k}...");
}
println!("Старт!");

Разбор по строкам

  • for i in 0..3 { — переменная i получает по очереди 0, 1 и 2. Это ровно те номера, которые нужны для индексов массива из трёх элементов.
  • expenses[i] — берём элемент по номеру. Тип индекса Rust подберёт сам, нам об этом думать не нужно.
  • i + 1 — для людей счёт идёт с единицы, поэтому в печати прибавляем 1.
  • (1..=3).rev() — .rev() разворачивает диапазон задом наперёд (от слова reverse). Сам диапазон при этом берут в круглые скобки, иначе точка относилась бы только к тройке.
  • println!("Старт!"); — стоит после цикла и выполнится один раз.
Вывод
Трата 1: 350 руб.
Трата 2: 1200 руб.
Трата 3: 99 руб.
3...
2...
1...
Старт!

Что выбрать

Знаете, сколько раз повторять, или есть набор элементов — берите for. Знаете только условие «пока» — while. Нужно крутиться неизвестно сколько и выйти по сложному условию внутри, или вернуть значение — loop. В Rust for используют чаще всего: он короче и защищает от ошибок со счётчиком.

Сумма в цикле: накопитель

Теперь вернёмся к нашей копилке. Чтобы сложить числа, мы используем приём, который встречается в программировании постоянно: накопитель (аккумулятор). Это обычная изменяемая переменная, которая стартует с нуля, и в неё на каждом круге добавляется очередное значение. Представьте стакан, в который по одной сыплют монетки: после каждой в стакане становится больше.

Перепишем функцию total с помощью цикла. Теперь она не зависит от того, что в массиве именно три элемента, а в самой записи нет трёх одинаковых кусков.

Rust · main.rs
fn total(expenses: [u32; 3]) -> u32 {
    let mut sum = 0;
    for e in expenses {
        sum += e;
    }
    sum
}

Разбор по строкам

  • let mut sum = 0; — накопитель. Он должен меняться, поэтому mut. Тип u32 Rust выведет сам из того, что мы вернём sum из функции с результатом u32.
  • for e in expenses { sum += e; } — на каждом круге добавляем очередную трату. После трёх кругов в sum лежит 1649.
  • sum — последнее выражение без ; — результат функции, как мы учили в уроке про функции.

Тот же приём подходит для поиска самого большого значения. Накопитель здесь хранит не сумму, а «рекорд на данный момент»: если встретилось число больше рекорда, запоминаем его. Так появляется вторая функция проекта.

Rust · main.rs
fn max_expense(expenses: [u32; 3]) -> u32 {
    let mut max = 0;
    for e in expenses {
        if e > max {
            max = e;
        }
    }
    max
}

Разбор по строкам

  • let mut max = 0; — стартовый рекорд. Суммы у нас не бывают отрицательными (u32), поэтому ноль подходит: любая трата будет не меньше.
  • if e > max { max = e; } — если текущая трата больше рекорда, записываем её как новый рекорд. Здесь вспоминаем if из прошлого урока, внутри цикла он работает так же.
  • max — после обхода всего массива возвращаем найденный рекорд.

Метки циклов: выйти сразу из двух

Когда один цикл вложен в другой, break выходит только из ближайшего, внутреннего. Чтобы выйти сразу из внешнего, циклу дают имя-метку: апостроф, имя и двоеточие.

Rust · main.rs
'outer: for a in 1..=3 {
    for b in 1..=3 {
        if a * b == 4 {
            println!("нашли: {a} и {b}");
            break 'outer;
        }
    }
}

Разбор по строкам

  • 'outer: for a in 1..=3 { — 'outer — метка внешнего цикла; имя выбираете вы сами.
  • for b in 1..=3 { — внутренний цикл: для каждого a перебирает b от 1 до 3.
  • break 'outer; — выходит из внешнего цикла, а значит, из обоих сразу. Без метки закончился бы только внутренний цикл, и внешний пошёл бы на следующий круг. Напечатается «нашли: 2 и 2».

Пока достаточно знать, что так можно

В нашей «Копилке» вложенные циклы понадобятся нескоро, поэтому метки пока просто держите в уме. Когда встретится вложенный цикл, из которого надо выйти разом, вы будете знать, где искать решение.

Практика: total и max_expense в Копилке

Обновим наш проект kopilka. Откройте файл src/main.rs и приведите его к виду ниже. Функция total теперь работает через for, появилась max_expense. Всё остальное — print_header, percent, безопасная left, print_status — остаётся таким, каким вы оставили в прошлых уроках, а в main добавилась одна строка с вызовом новой функции.

Шаг 1. Обновите main.rs

Rust · main.rs
const MONTH_LIMIT: u32 = 30_000;

fn print_header() {
    println!("Копилка 0.1");
    println!("Учёт расходов за месяц");
    println!();
}

fn total(expenses: [u32; 3]) -> u32 {
    let mut sum = 0;
    for e in expenses {
        sum += e;
    }
    sum
}

fn max_expense(expenses: [u32; 3]) -> u32 {
    let mut max = 0;
    for e in expenses {
        if e > max {
            max = e;
        }
    }
    max
}

fn percent(spent: u32, limit: u32) -> u32 {
    spent * 100 / limit
}

fn left(limit: u32, spent: u32) -> u32 {
    if spent > limit {
        0
    } else {
        limit - spent
    }
}

fn print_status(spent: u32, limit: u32) {
    if spent > limit {
        println!("Статус: лимит превышен");
    } else if spent * 100 >= limit * 80 {
        println!("Статус: почти лимит");
    } else {
        println!("Статус: в норме");
    }
}

fn main() {
    print_header();

    let expenses = [350, 1200, 99];
    let spent = total(expenses);
    let reserve = left(MONTH_LIMIT, spent);
    let note = if reserve == 0 { "запаса нет" } else { "запас есть" };
    let percent_exact = spent as f64 * 100.0 / MONTH_LIMIT as f64;

    println!("Лимит месяца:  {:>6} руб.", MONTH_LIMIT);
    println!("Потрачено:     {:>6} руб.", spent);
    println!("Осталось:      {:>6} руб. ({})", reserve, note);
    println!("Использовано лимита: {}%", percent(spent, MONTH_LIMIT));
    println!("Точнее: {:.1}%", percent_exact);
    println!("Самая большая трата: {} руб.", max_expense(expenses));
    print_status(spent, MONTH_LIMIT);
}

Разбор по строкам

  • fn total — складывает все траты в накопителе sum.
  • fn max_expense — ищет наибольшую трату: max хранит рекорд.
  • fn print_header, fn percent, fn left, fn print_status — без изменений: шапка, целый процент, безопасный остаток и статус по порогу 80 % от лимита.
  • let spent = total(expenses); — результат складываем в переменную, чтобы использовать и в печати, и в print_status.
  • max_expense(expenses) — вызываем прямо внутри println!: результат подставится вместо {}. Это единственная новая строка в main; остальное — как в конце прошлого урока.

Шаг 2. Запустите

В терминале, в папке kopilka:

Терминал
cargo run

Должно получиться:

Вывод
Копилка 0.1
Учёт расходов за месяц

Лимит месяца:   30000 руб.
Потрачено:       1649 руб.
Осталось:       28351 руб. (запас есть)
Использовано лимита: 5%
Точнее: 5.5%
Самая большая трата: 1200 руб.
Статус: в норме

Проверьте себя

  • Замените массив на [350, 29_000, 99]. Какая трата станет самой большой и какой статус напечатается? (Должно получиться 29000 и «почти лимит».)
  • Добавьте в main цикл for i in 0..3, который печатает номер и сумму каждой траты, как в разделе про диапазоны.
  • Замените 0..3 на 0..=3 и запустите. Что скажет программа и почему? Верните, как было.
  • Уберите слово mut у переменной sum и посмотрите, какую ошибку покажет компилятор.

Частые ошибки

0..3 не включает тройку

Что видно: цикл прошёл на один круг меньше, чем вы ждали, и последний элемент не обработан. Почему: конец диапазона не входит в него, 0..3 даёт 0, 1, 2. Как исправить: для включения конца пишите 1..=3 (с =) либо увеличьте конец.

Обратная ошибка опаснее: for i in 0..=3 для массива из трёх элементов дойдёт до индекса 3, которого нет. Программа соберётся, но упадёт при запуске:

Вывод
thread 'main' panicked at src/main.rs:5:16:
index out of bounds: the len is 3 but the index is 3

Это называется паникой: Rust останавливает программу, а не читает чужую память. Как исправить: вернуть 0..3 или, надёжнее, ходить по массиву через for e in expenses, где выйти за границы невозможно.

Бесконечный loop

Что видно: программа запущена, но ничего не заканчивается, терминал «завис». Почему: в loop нет break, либо условие выхода не может выполниться (например, забыли n += 1). То же бывает у while, если внутри не меняется то, что проверяется в условии. Как исправить: нажать Ctrl+C, затем проверить, что внутри цикла есть строка, которая рано или поздно приводит к break или к ложному условию.

Счётчик без mut

Что видно: цикл не собирается. Почему: накопитель меняется на каждом круге, а переменная объявлена как let sum = 0;.

Ошибка компилятора
error[E0384]: cannot assign twice to immutable variable `sum`
 --> src/main.rs:5:9
  |
3 |     let sum = 0;
  |         --- first assignment to `sum`
4 |     for e in expenses {
5 |         sum += e;
  |         ^^^^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
3 |     let mut sum = 0;
  |         +++

Как исправить: написать let mut sum = 0;. Компилятор подсказывает это сам в блоке help.

Что важно запомнить

  • loop крутится, пока его не остановит break; только он умеет вернуть значение: break 5;.
  • continue пропускает остаток текущего круга, break выходит из цикла совсем.
  • while условие повторяет, пока условие (типа bool) верно; счётчик в нём нужно менять самому.
  • for e in expenses обходит массив сам, без индексов и риска выйти за границы.
  • 0..3 даёт 0, 1, 2 (конец не входит), 1..=3 даёт 1, 2, 3, а .rev() идёт с конца.
  • Накопитель — это let mut переменная вне цикла: в ней копят сумму или рекорд.
  • Метка 'outer позволяет выйти из внешнего цикла командой break 'outer;.

Следующий шаг — практикум: мы научим «Копилку» читать суммы с клавиатуры в цикле, пока пользователь не напишет «стоп».

На следующий урок