Функции у нас уже есть, ветвления тоже, но функция total всё ещё складывает три траты вручную: expenses[0] + expenses[1] + expenses[2]. Появится четвёртая трата, или сотая, и такую запись придётся переписывать. Не хватает умения говорить программе «повтори это столько раз, сколько нужно», и именно этим мы займёмся.
loop: крутимся, пока не скажем «стоп»
Представьте проигрыватель, у которого включён режим «повтор». Песня доигрывает и тут же начинается снова, и так до бесконечности, пока вы сами не нажмёте «стоп». Самый простой цикл в Rust работает ровно так.
Официально это цикл (loop): блок кода, который выполняется снова и снова. Начинается он со слова loop, а выйти из него можно только командой break (по-английски «сломать, прервать»). Смотрим пример.
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.
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, а не просто число.
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, и он работает с любым набором элементов: массивом, диапазоном и, позже, другими коллекциями. Один проход цикла называют итерацией.
По массиву
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..3 | 0, 1, 2 | 3 |
1..=3 | 1, 2, 3 | 3 |
(0..3).rev() | 2, 1, 0 | 3 |
5..5 | пусто | 0 |
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 с помощью цикла. Теперь она не зависит от того, что в массиве именно три элемента, а в самой записи нет трёх одинаковых кусков.
fn total(expenses: [u32; 3]) -> u32 {
let mut sum = 0;
for e in expenses {
sum += e;
}
sum
}
Разбор по строкам
let mut sum = 0;— накопитель. Он должен меняться, поэтомуmut. Типu32Rust выведет сам из того, что мы вернёмsumиз функции с результатомu32.for e in expenses { sum += e; }— на каждом круге добавляем очередную трату. После трёх кругов вsumлежит 1649.sum— последнее выражение без;— результат функции, как мы учили в уроке про функции.
Тот же приём подходит для поиска самого большого значения. Накопитель здесь хранит не сумму, а «рекорд на данный момент»: если встретилось число больше рекорда, запоминаем его. Так появляется вторая функция проекта.
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 выходит только из ближайшего, внутреннего. Чтобы выйти сразу из внешнего, циклу дают имя-метку: апостроф, имя и двоеточие.
'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
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;.
Следующий шаг — практикум: мы научим «Копилку» читать суммы с клавиатуры в цикле, пока пользователь не напишет «стоп».