Начинали вы с приветствия в одну строку и трёх чисел в массиве, а теперь у вас программа из пяти файлов: она разбирает команды, объясняет ошибки, хранит траты в файле и проверена тестами. Осталось собрать версию «для людей», причесать код и оглянуться на пройденный путь.
Релизная сборка
Всё это время вы запускали проект через cargo run. Cargo при этом собирал «черновик»: быстро, с запасом отладочной информации и без оптимизаций. Это как репетиция спектакля в рабочей одежде: можно остановиться, заглянуть за кулисы, поправить. Для премьеры нужна другая сборка: отточенная, быстрая и компактная, но готовится она дольше.
Официально это называется профилем сборки (profile). У Cargo их два основных: dev (по умолчанию, его называют debug) и release. Чтобы собрать релизный, добавьте флаг --release:
cargo build --release
Compiling kopilka v0.1.0 (/home/user/kopilka)
Finished `release` profile [optimized] target(s) in 0.24s
Время у вас будет другим: на чистой сборке оно заметно больше. Готовая программа лежит не в корне проекта, а в папке target/release/: файл kopilka (на Windows — kopilka.exe). Папку target/ создаёт Cargo, в ней же лежит и черновая сборка в target/debug/.
Разбор по строкам
cargo build— собрать проект, но не запускать (в отличие отcargo run).--release— флаг профиля: включить оптимизации. Без него собирается debug.Compiling kopilka— компилятор собирает ваш пакет; в скобках путь к проекту.Finished `release` profile [optimized]— сборка успешна, профильrelease, оптимизации включены.
Чем отличаются две сборки
| debug (по умолчанию) | release | |
|---|---|---|
| Команда | cargo build или cargo run | cargo build --release или cargo run --release |
| Папка | target/debug/ | target/release/ |
| Сборка | быстрая | дольше, компилятор оптимизирует |
| Скорость программы | ниже | выше |
| Размер файла (у автора на macOS) | около 710 КБ | около 465 КБ |
Готовый файл можно скопировать на другой компьютер с такой же системой, и Rust там устанавливать не нужно: программа самостоятельная.
Ловушка: переполнение в release молчит
В debug-сборке переполнение целого числа (скажем, u32 перевалил за свой максимум) заканчивается паникой, а в release оно по умолчанию не проверяется и число «заворачивается» вокруг. Поэтому тестируйте и черновую, и релизную сборку, а границы проверяйте сами, как это делает MAX_AMOUNT в «Копилке».
А если запустить двойным кликом?
Файл можно открыть и из файлового менеджера, но папка, в которой в этот момент «находится» программа, зависит от системы и способа запуска. Надёжнее запускать из терминала, находясь там, где должен лежать kopilka.txt: имя файла в программе относительное, и Rust ищет его в текущей папке.
Порядок в коде: cargo fmt и cargo clippy
Представьте двух помощников. Первый — корректор: он не вникает в смысл, а расставляет пробелы, отступы и переносы по общему стандарту, чтобы чужой код читался как свой. Второй — опытный напарник: читает ваш код и подсказывает, как написать привычнее и надёжнее.
Официально первый называется rustfmt (запуск — cargo fmt), второй — Clippy (cargo clippy). Оба ставятся вместе с Rust через rustup. Если команда не находится, выполните rustup component add rustfmt clippy.
cargo fmt: единый вид
Допустим, в report.rs функция total записана небрежно:
pub fn total(expenses:&[Expense])->u32{ expenses.iter().map(|e| e.amount).sum::<u32>() }
Команда cargo fmt --check ничего не меняет, а только показывает, что именно переписал бы форматтер:
cargo fmt --check
Diff in /home/user/kopilka/src/report.rs:3:
use crate::MONTH_LIMIT;
use crate::expense::{Category, Expense};
-pub fn total(expenses:&[Expense])->u32{ expenses.iter().map(|e| e.amount).sum::<u32>() }
+pub fn total(expenses: &[Expense]) -> u32 {
+ expenses.iter().map(|e| e.amount).sum::<u32>()
+}
pub fn totals_by_category(expenses: &[Expense]) -> HashMap<Category, u32> {
let mut totals: HashMap<Category, u32> = HashMap::new();
Разбор по строкам
Diff in .../report.rs:3— файл и строка, с которой начинаются расхождения.- Строки с
-— как написано сейчас, строки с+— как будет после форматирования; строки с пробелом в начале не меняются. - Если расхождений нет, команда молчит и завершается успешно; при расхождениях она завершается с ошибкой, и это удобно для проверок.
Сам cargo fmt без --check перепишет файлы. Он меняет только вид кода, а не поведение программы.
cargo clippy: подсказки напарника
Допустим, вместо expenses.is_empty() в print_list написано expenses.len() == 0. Программа работает, но можно проще:
cargo clippy
Checking kopilka v0.1.0 (/home/user/kopilka)
warning: length comparison to zero
--> src/report.rs:29:8
|
29 | if expenses.len() == 0 {
| ^^^^^^^^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `expenses.is_empty()`
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#len_zero
= note: `#[warn(clippy::len_zero)]` on by default
warning: `kopilka` (bin "kopilka") generated 1 warning (run `cargo clippy --fix --bin "kopilka" -p kopilka -- ` to apply 1 suggestion)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.16s
Разбор по строкам
warning: length comparison to zero— суть замечания: длину сравнивают с нулём.--> src/report.rs:29:8— файл, строка и столбец.help: ... expenses.is_empty()— готовая замена.#[warn(clippy::len_zero)]— имя правила; по нему можно найти объяснение по ссылке выше.
Предупреждение не ошибка
Слово warning означает «можно лучше», а не «сломано»: сборка проходит, программа запускается. Прочитайте совет и решите сами. Команда cargo clippy --fix умеет применять простые советы автоматически, но сначала поймите, что она изменит. На вашем готовом проекте cargo clippy печатает только Checking и Finished: тишина здесь — хороший знак.
Карта проекта
Когда проект вырос до пяти файлов, полезно держать в голове «план дома»: что где лежит и зачем. Вот он (в скобках — где вы эту тему проходили):
| Файл | Что внутри | Откуда |
|---|---|---|
main.rs | подключение модулей, MONTH_LIMIT, print_help, run_command, главный цикл main | циклы, ввод, match |
expense.rs | Category (from_word, title), Expense (new, describe, to_line, from_line) | структуры и перечисления |
command.rs | Command, CommandError с Display, parse_command, тесты разбора | ошибки: Result и ? |
report.rs | total, totals_by_category, print_status, print_list, print_report, тесты | векторы, HashMap, итераторы |
storage.rs | save и load: чтение и запись kopilka.txt | std::fs и io::Result |
Путь одной команды
Проследите, что происходит, когда вы набираете добавить 350 еда обед:
mainчитает строку черезread_lineв переменнуюline(заимствование и строки).parse_commandизcommand.rsпревращает строку вOk(Command::Add(expense))или в понятную ошибкуCommandError.mainотдаёт команду вrun_command, а та черезmatchвыбирает действие.- Ветка
Command::Addкладёт трату в вектор, вызываетsaveизstorage.rsи печатает статус черезprint_statusизreport.rs.
А вот целиком main.rs. Остальные файлы вы уже знаете по прошлым урокам, а этот связывает их воедино. Лежит он в папке src рядом с остальными:
mod command;
mod expense;
mod report;
mod storage;
use std::io;
use command::{Command, parse_command};
use expense::Expense;
use report::{print_list, print_report, print_status, total};
use storage::{load, save};
pub const MONTH_LIMIT: u32 = 30_000;
const DATA_FILE: &str = "kopilka.txt";
fn print_help() {
println!("Команды:");
println!(" добавить <сумма> <категория> [заметка] - записать трату");
println!(" список - показать все траты");
println!(" отчёт - итоги по категориям");
println!(" помощь - показать эту подсказку");
println!(" выход - закончить работу");
println!("Категории: еда, транспорт, дом, отдых, другое");
}
fn run_command(command: Command, expenses: &mut Vec<Expense>) -> bool {
match command {
Command::Add(expense) => {
println!("Записано: {}", expense.describe());
expenses.push(expense);
if let Err(err) = save(DATA_FILE, expenses) {
println!("Не удалось сохранить: {err}");
}
let spent = total(expenses);
println!("Потрачено: {spent} руб. из {MONTH_LIMIT} руб.");
print_status(spent, MONTH_LIMIT);
}
Command::List => print_list(expenses),
Command::Report => print_report(expenses),
Command::Help => print_help(),
Command::Quit => return false,
}
true
}
fn main() {
println!("Копилка 0.7");
println!("Наберите помощь, чтобы увидеть команды.");
let mut expenses = match load(DATA_FILE) {
Ok(loaded) => loaded,
Err(err) => {
println!("Не удалось прочитать {DATA_FILE}: {err}");
Vec::new()
}
};
if !expenses.is_empty() {
println!("Загружено трат: {}", expenses.len());
}
let mut line = String::new();
loop {
line.clear();
let bytes = io::stdin()
.read_line(&mut line)
.expect("не удалось прочитать строку");
if bytes == 0 {
break;
}
match parse_command(&line) {
Ok(command) => {
if !run_command(command, &mut expenses) {
break;
}
}
Err(err) => println!("Ошибка: {err}"),
}
}
println!(
"Итого потрачено: {} руб. из {MONTH_LIMIT} руб.",
total(&expenses)
);
}
Разбор по строкам
mod command;и ещё три строки — подключают файлы-модули; без них Rust не узнает, что они есть.use ...— приносят нужные имена из модулей, чтобы писатьparse_command, а неcommand::parse_command.pub const MONTH_LIMIT— лимит месяца, видный и изreport.rs(use crate::MONTH_LIMIT);DATA_FILE— имя файла, относительное к текущей папке.print_help— печатает список команд;run_command— выполняет одну разобранную команду и возвращаетfalseтолько дляQuit.let mut expenses = match load(DATA_FILE)— при старте загружаем траты, а при ошибке чтения печатаем причину и начинаем с пустого вектора.- цикл
loop:line.clear()очищает буфер,read_lineвозвращает 0 байт на конце ввода (тогдаbreak), дальшеparse_commandиmatchпоOk/Err. - последний
println!— итог при выходе.
Что можно добавить самим
Лучший способ закрепить курс — доделать проект по-своему. Три идеи от простой к посложнее, всё нужное вы уже умеете:
- Команда
удалить N. Нужен новый вариант вCommand, разбор номера вparse_command(с понятной ошибкой, как у суммы), ветка вrun_commandи сохранение файла после удаления. Помните, что в списке номера начинаются с 1, а в векторе — с 0, и что выход за границы вектора ведёт к панике: метод удаления найдите в документацииVecпо ссылкам из Книги. - Лимит по категориям. Функция с
matchпоCategory, которая возвращает свой лимит для каждой категории;print_reportможет помечать категории, где он превышен. Заодно напишете для неё тест вreport.rs. - Месяц в строке файла. Строка вида
2026-11;350;еда;обедпозволит считать итоги по месяцам. Сложность в том, что старые файлы со строкой из трёх частей не должны ломаться:from_lineпридётся научить читать оба вида, а тесты — проверять оба.
Каждую правку начинайте с теста, а заканчивайте прогоном fmt, clippy и test.
Куда дальше
Вы познакомились с основами: владение и заимствование, структуры и перечисления, коллекции, ошибки, модули и тесты. Дальше есть куда расти:
- Курс Rust на sudoteach.com — обобщения, трейты, времена жизни, умные указатели, потоки и итоговый веб-сервер.
- Курс Cargo на sudoteach.com — подробно про Cargo: подключение крейтов (готовых библиотек), настройка профилей, workspace для проектов из нескольких пакетов.
- Книга «The Rust Programming Language» — официальная и бесплатная; к ней удобно возвращаться за подробностями по любой теме курса.
Практика: финальный прогон
Берём проект kopilka в том виде, в каком он остался после урока про тесты (пять файлов в src). Код менять не нужно: мы проверим его и соберём.
Шаг 1. Чистый старт
Откройте терминал в папке проекта (там, где лежит Cargo.toml). Если от прошлых запусков остался kopilka.txt, удалите его (rm kopilka.txt, в Windows — del kopilka.txt), чтобы сценарий шёл с нуля.
Шаг 2. Порядок и тесты
Запустите cargo fmt --check (молчит) и cargo clippy (только Checking и Finished), затем cargo test:
running 10 tests
test command::tests::parse_empty_line ... ok
test command::tests::parse_add_with_note ... ok
test command::tests::parse_bad_amount ... ok
test command::tests::parse_add_without_note ... ok
test command::tests::parse_missing_args_and_category ... ok
test command::tests::parse_simple_commands ... ok
test report::tests::total_of_empty_list_is_zero ... ok
test command::tests::parse_unknown_command ... ok
test report::tests::total_sums_all_amounts ... ok
test report::tests::totals_group_by_category ... ok
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Шаг 3. Релиз
Соберите релизную версию командой cargo build --release (вывод вы видели выше). В папке target/release/ появится kopilka.
Шаг 4. Сценарий
Запустите готовую программу из папки проекта. Можно набрать команды руками, а в macOS и Linux — подать их разом через конвейер (в Windows printf нет, набирайте вручную и запускайте target\release\kopilka.exe):
printf 'добавить 350 еда обед в столовой\nдобавить 1200 транспорт проездной\nдобавить 99 отдых кино\nсписок\nотчёт\nдобавить abc еда\nпицца\nвыход\n' | ./target/release/kopilka
Копилка 0.7
Наберите помощь, чтобы увидеть команды.
Записано: 350 руб., еда, «обед в столовой»
Потрачено: 350 руб. из 30000 руб.
Статус: в норме
Записано: 1200 руб., транспорт, «проездной»
Потрачено: 1550 руб. из 30000 руб.
Статус: в норме
Записано: 99 руб., отдых, «кино»
Потрачено: 1649 руб. из 30000 руб.
Статус: в норме
1. 350 руб., еда, «обед в столовой»
2. 1200 руб., транспорт, «проездной»
3. 99 руб., отдых, «кино»
транспорт: 1200 руб. 72%
еда: 350 руб. 21%
отдых: 99 руб. 6%
Всего: 1649 руб. из 30000 руб.
Статус: в норме
Ошибка: «abc» — не сумма
Ошибка: Не знаю команду «пицца»
Итого потрачено: 1649 руб. из 30000 руб.
Разбор по строкам
- Первые три команды дописывают траты; после каждой видны «Потрачено» и «Статус».
списокпечатает траты с номерами,отчёт— суммы по категориям по убыванию, с долей в процентах.добавить abc едаипиццадают понятныеОшибка: ..., программа не падает.выходзавершает цикл, и печатается итог.
Шаг 5. Данные сохранились
В папке проекта появился файл kopilka.txt:
350;еда;обед в столовой
1200;транспорт;проездной
99;отдых;кино
Запустите программу ещё раз и наберите список и выход: она сама скажет, сколько трат загружено.
Копилка 0.7
Наберите помощь, чтобы увидеть команды.
Загружено трат: 3
1. 350 руб., еда, «обед в столовой»
2. 1200 руб., транспорт, «проездной»
3. 99 руб., отдых, «кино»
Итого потрачено: 1649 руб. из 30000 руб.
Проверьте себя
cargo fmt --checkиcargo clippyотработали без замечаний, аcargo testпоказал10 passed.- Файл
target/release/kopilka(илиkopilka.exe) существует, и программа запускается из терминала. - После выхода в папке, откуда запускали, лежит
kopilka.txtс тремя строками. - Второй запуск пишет
Загружено трат: 3. - Вы можете объяснить своими словами, что делает каждый из пяти файлов.
Частые ошибки
1. Релиз запущен из другой папки
Что видно: вы вернулись в домашнюю папку, запустили ~/kopilka/target/release/kopilka и увидели Пока нет трат, хотя вчера записывали. Почему: DATA_FILE — относительное имя, и программа берёт kopilka.txt из текущей папки, а не из папки, где лежит сам файл программы. В другой папке она тихо создаст новый файл. Как исправить: запускайте из той же папки, а чтобы запись попала в нужный файл, перейдите в неё командой cd перед запуском.
Копилка 0.7
Наберите помощь, чтобы увидеть команды.
Пока нет трат
Итого потрачено: 0 руб. из 30000 руб.
2. Запущена не та сборка
Что видно: вы поправили код, собрали cargo build --release, но запускаете файл из target/debug/ и не видите изменений (или наоборот). Почему: у профилей разные папки, и каждая хранит свою последнюю сборку. Как исправить: после правки пересоберите нужный профиль и запускайте файл из его папки, либо используйте cargo run / cargo run --release.
3. Предупреждения Clippy приняты за ошибки
Что видно: красные строки с warning, и кажется, что проект сломан. Почему: предупреждение — совет, а не запрет: сборка и запуск проходят. Как исправить: прочитайте строку help и откройте ссылку на правило. Применяйте совет, если он вам понятен.
Что важно запомнить
cargo build --releaseсобирает оптимизированную версию вtarget/release/; обычная сборка (debug) лежит вtarget/debug/и собирается быстрее.- Релиз нужно запускать из той папки, где должен лежать
kopilka.txt: имя файла в программе относительное. - В release переполнение чисел не вызывает панику, поэтому границы данных проверяйте сами.
cargo fmtприводит код к единому виду (--checkтолько показывает расхождения),cargo clippyдаёт советы; предупреждения — не ошибки.- Проект из пяти файлов держится на разделении ролей: ввод и цикл —
main.rs, данные —expense.rs, команды и ошибки —command.rs, итоги —report.rs, файл —storage.rs. - Привычка перед сдачей работы:
cargo fmt,cargo clippy,cargo test,cargo build --release.
Вот и весь путь: от первой строки println! до собственной программы с тестами и сохранением данных. Спасибо, что дошли до конца, и удачи с Rust!