Мы умеем писать функции, ветвить программу через if и повторять действия в цикле. Но пока все траты зашиты в код: чтобы добавить новую, приходится править программу и собирать её заново. Сегодня «Копилка» впервые станет живой: она будет спрашивать суммы у вас, прямо в терминале.
Что мы сделаем
Сначала представим результат целиком. Так проще понять, зачем нужен каждый кусок кода. Вот сессия, которую мы хотим получить (то, что печатаете вы, идёт после запроса, остальное — ответы программы):
Копилка 0.2
Вводите траты по одной, для выхода напишите стоп.
Введите сумму (или стоп):
350
Записано: 350 руб. Всего: 350 руб.
Введите сумму (или стоп):
кофе
Это не число, попробуйте ещё раз
Введите сумму (или стоп):
1200
Записано: 1200 руб. Всего: 1550 руб.
Введите сумму (или стоп):
стоп
Итого потрачено: 1550 руб. из 30000 руб.
Осталось: 28450 руб.
Использовано лимита: 5%
Статус: в норме
Три поведения: сумма записывается, слово вместо числа вежливо отклоняется (программа не падает), слово «стоп» завершает цикл.
Читаем строку с клавиатуры
Представьте почтовый ящик: чтобы получить письмо, нужна коробка, куда его положат. Так и здесь: заводим пустую строку-коробку, и система кладёт в неё то, что вы набрали и подтвердили клавишей Enter. Официально это String — текст, которым программа владеет и который может менять. Подробно о строках — в отдельном модуле.
Читает с клавиатуры функция из модуля std::io (io — ввод-вывод), поэтому в начале файла пишем use std::io;: «хочу пользоваться тем, что лежит в io». Это шаблон, его можно запомнить как рецепт:
use std::io;
fn main() {
let mut line = String::new();
io::stdin()
.read_line(&mut line)
.expect("не удалось прочитать строку");
println!("Вы ввели: {line}");
}
Разбор по строкам
use std::io;— подключаем модуль ввода-вывода; без этой строки имяioкомпилятору неизвестно.let mut line = String::new();— создаём пустую строку-коробку.mutнужен, потому что мы будем её менять: класть туда текст.io::stdin()— стандартный ввод (standard input): всё, что вы набираете в терминале..read_line(&mut line)— метод «прочитай одну строку». Метод — действие, которое вызывают через точку. В скобках мы отдаём коробку;&mutчитается как «даём функции право записать в неё». Почему именно так, объясним в модуле про владение..expect("не удалось прочитать строку")— чтение может не получиться (например, ввод оборвали). Тогда программа остановится с этим сообщением. Подробно об ошибках — в отдельном модуле, пока берём как шаблон.println!("Вы ввели: {line}");— печатаем содержимое коробки.
Enter попадает в строку
Наберёте 350 и Enter, а в строке окажется 350 и символ конца строки \n. Эту особенность мы сейчас и будем обходить.
Ещё две детали. Первая: read_line дописывает текст в конец коробки, а не заменяет его. Если читать в цикле и не опустошать строку, вторая сумма приклеится к первой. Опустошает строку метод line.clear(): вызываем его в начале каждого прохода цикла. Вторая: read_line возвращает число прочитанных байтов (для кириллицы это больше, чем число букв). Ноль значит, что ввод закончился (нажали Ctrl+D или данные подали из файла). Тогда цикл надо остановить, иначе он будет крутиться вечно.
Из текста в число
В коробке лежит 350. Для Rust это пока три символа текста, а не число: складывать их нельзя, как нельзя положить в кассу фотографию банкноты. Текст нужно разобрать и получить настоящее число. Официально такая операция называется парсингом (parse — «разобрать»), и выполняет её метод parse.
У парсинга два возможных исхода: получилось или нет. Пользователь мог ввести и 350, и кофе. Поэтому parse возвращает не число, а Result (результат), у которого два варианта: Ok(число), если разобрать удалось, и Err(причина), если нет. Чтобы достать число, мы ставим развилку match: она, как if, выбирает ветку, но по варианту результата. Это снова шаблон; подробно Result и ошибки разберём в модуле про ошибки.
let amount = match line.trim().parse::<u32>() {
Ok(n) => n,
Err(_) => {
println!("Это не число, попробуйте ещё раз");
continue;
}
};
Разбор по строкам
line.trim()— убирает пробелы и перевод строки по краям текста. Без этого"350\n"не разберётся: символ\nне цифра..parse::<u32>()— просим разобрать текст как число типаu32(целое неотрицательное, как все наши суммы). Часть::<u32>указывает, во что превращаем.match ... {— развилка по результату разбора.Ok(n) => n,— если получилось, число называемn, и всё выражениеmatchстановится равноn: оно и попадает вamount.Err(_) => { ... }— если не получилось, подробности нам не нужны (подчёркивание значит «неважно что»). Печатаем сообщение и делаемcontinue.continue;— знакомая команда цикла: бросить текущий проход и сразу начать следующий. До сложения дело не дойдёт.
Собираем цикл
Теперь у нас есть все детали. Вспомним цикл loop: он повторяется бесконечно, пока мы сами не скажем break. Это то, что нужно: мы не знаем, сколько трат введёт пользователь. Тело цикла получится таким:
- Напечатать приглашение и очистить коробку:
line.clear(). - Прочитать строку. Если прочитано ноль байтов, ввод закончился:
break. - Если очищенный текст равен
стоп, тожеbreak. - Разобрать текст в число по шаблону с
match; при неудаче — сообщение иcontinue. - Прибавить число к накопителю:
spent += amount— и напечатать подтверждение.
После цикла печатаем итог, остаток, процент и вызываем print_status: функции left, percent и print_status остаются из прошлых уроков, как и print_header. Функции total и max_expense из урока про циклы мы удаляем: они считали по готовому массиву, а теперь суммы приходят по одной, и накопитель spent справляется сам. Массив вернётся позже в виде вектора.
Практика: собираем main.rs
- Откройте терминал в папке проекта
kopilka(там лежитCargo.toml). - Замените всё содержимое
src/main.rsкодом ниже. Если в вашем файле что-то называется иначе или слова другие, ничего страшного: берите код из урока целиком. - Выполните
cargo run. Программа напечатает приглашение и будет ждать. Введите350и нажмите Enter, затем1200, затем какое-нибудь слово и, наконец,стоп.
use std::io;
const MONTH_LIMIT: u32 = 30_000;
fn print_header() {
println!("Копилка 0.2");
println!("Вводите траты по одной, для выхода напишите стоп.");
}
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 mut spent = 0;
let mut line = String::new();
loop {
println!("Введите сумму (или стоп):");
line.clear();
let bytes = io::stdin()
.read_line(&mut line)
.expect("не удалось прочитать строку");
if bytes == 0 {
break;
}
if line.trim() == "стоп" {
break;
}
let amount = match line.trim().parse::<u32>() {
Ok(n) => n,
Err(_) => {
println!("Это не число, попробуйте ещё раз");
continue;
}
};
spent += amount;
println!("Записано: {amount} руб. Всего: {spent} руб.");
}
println!("Итого потрачено: {spent} руб. из {MONTH_LIMIT} руб.");
println!("Осталось: {} руб.", left(MONTH_LIMIT, spent));
println!("Использовано лимита: {}%", percent(spent, MONTH_LIMIT));
print_status(spent, MONTH_LIMIT);
}
Разбор по строкам
fn print_header()— та же функция шапки, но тексты в ней обновлены: версия теперь0.2, потому что программа стала интерактивной, а вместо подзаголовка — подсказка простоп.fn percent,fn left,fn print_status— функции из уроков про функции и условия, без изменений.print_statusсначала проверяет самое плохое (spent > limit), затем «80 процентов и больше» (spent * 100 >= limit * 80), иначе «в норме».let mut spent = 0;— накопитель; типu32Rust выведет сам.let mut line = String::new();— одна коробка на весь цикл, создаём её доloop.line.clear();— опустошаем коробку, чтобы новый ввод не приклеился к старому.let bytes = ...expect(...);— тот же шаблон чтения, но результат сохраняем: ноль байтов значит, что ввод закончился, и делаемbreak.if line.trim() == "стоп" { break; }— сравниваем очищенный текст со словом;==спрашивает «равно ли?».let amount = match ... ;— разбор числа по шаблону из прошлого раздела.spent += amount;— прибавляем сумму к накопителю.left(MONTH_LIMIT, spent)иpercent(spent, MONTH_LIMIT)— после цикла показываем остаток и долю лимита, как в прошлых уроках.print_status(spent, MONTH_LIMIT);— после цикла выводим итоговый статус.
Если неудобно набирать всё руками, подайте ввод одной командой (macOS и Linux; в Windows PowerShell вместо printf подойдёт "350`n1200`nстоп" | cargo run):
printf '350\n1200\nкофе\n99\nстоп\n' | cargo run -q
Ключ -q прячет служебные сообщения Cargo. Результат:
Копилка 0.2
Вводите траты по одной, для выхода напишите стоп.
Введите сумму (или стоп):
Записано: 350 руб. Всего: 350 руб.
Введите сумму (или стоп):
Записано: 1200 руб. Всего: 1550 руб.
Введите сумму (или стоп):
Это не число, попробуйте ещё раз
Введите сумму (или стоп):
Записано: 99 руб. Всего: 1649 руб.
Введите сумму (или стоп):
Итого потрачено: 1649 руб. из 30000 руб.
Осталось: 28351 руб.
Использовано лимита: 5%
Статус: в норме
Как выйти из программы
Ctrl+C останавливает любую программу в терминале. Ctrl+D (в Windows Ctrl+Z и Enter) заканчивает ввод, и цикл спокойно завершится с итогом.
Проверьте себя
- Вы можете объяснить, зачем в цикле
line.clear(). - Слово «кофе» не роняет программу.
- Слово «стоп» завершает цикл и печатает итог.
- Итог и статус печатаются один раз, после цикла.
Частые ошибки
1. Забыли line.clear()
Что видно. Первая сумма принимается, а все следующие объявляются не числами. Если ввести 350, потом 1200, программа ответит:
Введите сумму (или стоп):
Записано: 350 руб. Всего: 350 руб.
Введите сумму (или стоп):
Это не число, попробуйте ещё раз
Почему. read_line дописывает текст в конец строки. После второго ввода в коробке лежит "350\n1200\n", и разобрать это как одно число нельзя. Как исправить. Добавьте line.clear(); в начало каждого прохода цикла, до read_line.
2. Забыли trim()
Что видно. Любая сумма, даже вполне нормальная, получает ответ «Это не число», а слово «стоп» не останавливает программу. Почему. В конце строки остался символ \n, и "350\n" не равно "350". Как исправить. Пишите line.trim() и при сравнении, и перед parse.
3. Потеряли mut
Что видно. Если объявить let line = String::new(); без mut, компилятор ругается:
error[E0596]: cannot borrow `line` as mutable, as it is not declared as mutable
--> src/main.rs:36:9
|
36 | let line = String::new();
| ^^^^ not mutable
...
40 | line.clear();
| ---- cannot borrow as mutable
...
43 | .read_line(&mut line)
| --------- cannot borrow as mutable
|
help: consider changing this to be mutable
|
36 | let mut line = String::new();
| +++
Почему. Строку мы меняем, а без mut переменная неизменяемая. Как исправить. let mut line, как подсказывает компилятор.
4. Нет &mut в read_line
Что видно. read_line(line) даёт error[E0308]: mismatched types с пометкой expected `&mut String`, found `String`. Почему. Функции нужно право записи в вашу коробку, а не сама коробка. Как исправить. Пишите read_line(&mut line), как подсказывает help: consider mutably borrowing here.
5. Программа зациклилась
Что видно. Без конца печатается «Введите сумму (или стоп):» и «Это не число». Почему. Ввод закончился, read_line возвращает пустую строку, parse на ней ошибается, continue начинает новый круг. Как исправить. Выходите по break, когда прочитано ноль байтов.
Огромные числа
Если ввести два раза 4000000000, сумма не поместится в u32, и программа остановится с паникой attempt to add with overflow (а при очень большом итоге то же случится в percent: attempt to multiply with overflow). Эти числа известны только во время работы, поэтому предупредить заранее компилятор не может. Защиту разберём в модуле про ошибки.
Что важно запомнить
use std::io;подключает ввод-вывод,String::new()создаёт пустую строку, аlet mutнужен, потому что строку мы меняем.io::stdin().read_line(&mut line).expect("...")— шаблон чтения строки: Enter остаётся в конце строки как\n.read_lineдописывает, а не заменяет, поэтому в цикле первой строкой идётline.clear().- Результат
read_line— число прочитанных байтов; ноль означает, что ввод закончился, и цикл надо прервать. line.trim()убирает пробелы и\nпо краям; без него сравнение со словом иparseне сработают.parse::<u32>()возвращаетOk(число)илиErr(_); шаблон сmatchиcontinueзащищает программу от плохого ввода.
Следующий шаг — модуль про владение: мы наконец разберёмся, что на самом деле значит &mut в read_line и почему Rust так строго следит за тем, кто чем владеет.