Rust: владение, заимствование и первая программа CLI
На этой странице
Rust проверяет правила использования памяти во время компиляции с помощью владения и проверки типов. В отличие от Java и Kotlin, где сборщик мусора отслеживает достижимость объектов, код выражает, кому принадлежит значение и на какой срок оно заимствовано. Это позволяет воспринимать ошибки компилятора как контракты использования данных, а не только как ограничения синтаксиса.
Это введение рассчитано на разработчиков, которые уже писали функции на другом языке. Примеры выполнены с Rust 1.100.0, edition 2024, на macOS arm64. Редакция выбирает правила синтаксиса и совместимости независимо от версии компилятора. Примеры используют только стандартную библиотеку и не обращаются к сети.
Для каких задач подходит Rust?
Rust компилируется в машинный код и не требует трассирующего сборщика мусора для стандартного управления памятью. Его стоит рассмотреть для CLI, системных компонентов и программ с важными ограничениями по памяти или задержке. Официальное введение также называет производительность и безопасность памяти основными целями. Само отсутствие GC не доказывает, что Rust быстрее Java или обеспечивает меньшую задержку. Сравнительная производительность в этой статье не измерялась.
| Критерий | Java/Kotlin в JVM | Rust |
|---|---|---|
| Типичное управление памятью объектов | GC отслеживает объекты для освобождения | Ресурсы освобождаются через владение и Drop |
| Передача данных | Ссылки на объект могут храниться в нескольких местах | Значения перемещаются или копируются в зависимости от типа; их можно заимствовать по ссылке |
| Передача ошибок | Исключения, типы результатов и другие механизмы | API часто выражают успех или ошибку через Result<T, E> |
| Издержки внедрения | Существующие JVM-библиотеки и опыт команды | Изучение владения, пригодность библиотек и инструментов сборки и эксплуатации |
Если узкое место существующего сервиса — ожидание базы данных или внешнего API, сначала измерьте эти пути и лишь затем рассматривайте смену языка. Для первого внедрения начните с небольшой CLI с чёткими границами и оцените затраты на разработку и развёртывание.
Чем владение отличается от заимствования?
Сначала установите rustup и Rust по официальной инструкции для вашей операционной системы, затем выполните примеры. На macOS также установите Command Line Tools, необходимые для нативной компоновки.
String владеет строковыми данными. Сохраните следующий код как move_error.rs: он намеренно не компилируется.
fn main() {
let name = String::from("Rust");
let moved = name;
println!("{name}, {moved}");
}
rustc --edition 2024 move_error.rs
Поскольку let moved = name передаёт владение, следующая попытка вывести name приводит к E0382: borrow of moved value. Не каждое присваивание означает перемещение: типы с Copy, например u64, копируются при присваивании. String::clone() отдельно копирует данные строки, поэтому применяйте его при необходимости. Подробности приведены в официальном описании владения.
Если значение нужно только прочитать, его можно заимствовать через &, не передавая владение. Сохраните следующий код в отдельный файл borrow_ok.rs.
fn main() {
let mut name = String::from("Rust");
let borrowed = &name;
println!("{borrowed}");
name.push_str(" language");
println!("{name}");
}
rustc --edition 2024 borrow_ok.rs -o borrow_ok
./borrow_ok
Фактический вывод:
Rust
Rust language
После первого println! переменная borrowed больше не используется, и общее заимствование заканчивается. Поэтому name можно изменить позже в том же блоке. Если снова вывести borrowed после push_str, период, в течение которого нужна ссылка, пересечётся с исключительным заимствованием для изменения, и компиляция завершится ошибкой.
Для начала удобно понимать правило так: к одним данным допустимы несколько общих ссылок &T или одна исключительная ссылка &mut T. Это не означает, что &T запрещает всю внутреннюю изменяемость. Подробные условия описаны в официальной документации по заимствованию. Lifetime выражает отношения между периодами, в которые ссылки должны быть действительны; сам по себе он не продлевает жизнь памяти.
Создаём CLI для суммирования с Cargo
Программа выводит количество и сумму целочисленных значений. Это учебный пример, а не платёжная логика конкретного сервиса; предполагается, что все входные суммы имеют одну единицу измерения. Дробные и отрицательные значения не принимаются; каждый аргумент и итог должны помещаться в u64.
Эти команды рассчитаны на shell macOS или Linux. Подготовьте версию для упражнения в среде с установленными инструментами Rust.
rustup toolchain install 1.100.0 --profile minimal --component rustfmt --component clippy
cargo +1.100.0 new expense-summary --bin --edition 2024
cd expense-summary
Создайте три файла ниже. rust-toolchain.toml фиксирует версию инструментов для этой папки, а Cargo.toml содержит настройки пакета. Команды сборки и проверки Cargo также описаны в официальном введении в Cargo.
Cargo.toml
[package]
name = "expense-summary"
version = "0.1.0"
edition = "2024"
[dependencies]
rust-toolchain.toml
[toolchain]
channel = "1.100.0"
profile = "minimal"
components = ["rustfmt", "clippy"]
Диаграмма показывает цикл и досрочный возврат в summarize. Линия слева возвращается к проверке следующего значения после успешного накопления; все пути ошибок справа сходятся в ветви Err функции main.
src/main.rs
use std::{env, process::ExitCode};
struct Summary {
count: usize,
total: u64,
}
#[derive(Debug)]
enum InputError {
EmptyInput,
InvalidAmount,
TotalOverflow,
}
fn summarize(inputs: &[String]) -> Result<Summary, InputError> {
if inputs.is_empty() {
return Err(InputError::EmptyInput);
}
let mut total = 0_u64;
for input in inputs {
let amount = input
.parse::<u64>()
.map_err(|_| InputError::InvalidAmount)?;
total = total.checked_add(amount).ok_or(InputError::TotalOverflow)?;
}
Ok(Summary {
count: inputs.len(),
total,
})
}
fn main() -> ExitCode {
let inputs: Vec<String> = env::args().skip(1).collect();
match summarize(&inputs) {
Ok(summary) => {
println!("count={}, total={}", summary.count, summary.total);
ExitCode::SUCCESS
}
Err(error) => {
eprintln!("error: {error:?}");
ExitCode::FAILURE
}
}
}
inputs — это принадлежащий main вектор Vec<String>. summarize(&inputs) заимствует его строки как срез &[String], не копируя все данные и не передавая владение. for input in inputs также читает строки по ссылке. В этом примере заимствование позволяет не копировать входные строки, но не устраняет выделения памяти при сборе аргументов командной строки.
Summary — структура результата при успехе, а InputError — перечисление возможных ошибок. Result<Summary, InputError> задаёт контракт возврата одного из двух результатов. ? досрочно возвращает управление из текущей функции при Err, а при успехе извлекает внутреннее значение. Он не выбрасывает исключение и не завершает программу немедленно. Правила распространения ошибок изложены в официальном описании Result.
parse::<u64>() проверяет формат и диапазон каждого числа. Если сумма двух значений выходит за диапазон, checked_add возвращает None, а ok_or преобразует его в ошибку TotalOverflow. Поэтому даже допустимые по отдельности входные значения могут дать ошибку суммы. main обрабатывает оба результата через match, а {:?} выводит имя ошибки в формате Debug.
Что можно проверить при запуске?
rustc --version
cargo check --offline
cargo fmt --check
cargo clippy --offline -- -D warnings
cargo run --offline --quiet -- 12000 8000 0
Стандартный вывод этой команды приведён ниже. Значения после -- передаются программе примера, а не Cargo.
count=3, total=20000
Это характерные результаты реальных запусков CLI. При ошибке промежуточная сумма не выводится; ошибки направляются в стандартный поток ошибок, а код завершения равен 1.
Аргументы после cargo run --offline --quiet -- |
Вывод | Код завершения |
|---|---|---|
12000 8000 0 |
stdout count=3, total=20000 |
0 |
0 |
stdout count=1, total=0 |
0 |
| Нет аргументов | stderr error: EmptyInput |
1 |
12000 abc |
stderr error: InvalidAmount |
1 |
-1 или 1.5 |
stderr error: InvalidAmount |
1 |
18446744073709551616 |
stderr error: InvalidAmount |
1 |
18446744073709551615 0 |
stdout count=2, total=18446744073709551615 |
0 |
18446744073709551615 1 |
stderr error: TotalOverflow |
1 |
cargo check проверяет типы и другие свойства, не запуская код. cargo fmt --check проверяет форматирование, а Clippy выполняет статическую диагностику. Даже при их успехе отдельно запустите программу, чтобы проверить правила расчёта. Здесь сравнивались вывод, ошибки и коды завершения в режимах debug и release; пропускная способность и производственная нагрузка не измерялись.
Что гарантирует Rust?
Проверки типов и владения в Safe Rust помогают предотвратить ошибочное использование ссылок и гонки данных. Возможность переноса значений между потоками и совместного использования ссылок также связана с контрактами типов, такими как Send и Sync. Это не устраняет все состояния гонки, взаимные блокировки, утечки памяти или ошибки бизнес-логики.
unsafe возлагает условия безопасности некоторых операций на автора кода. Это не переключатель, отключающий всю проверку заимствований; неверная реализация unsafe или границы FFI могут повлиять на безопасность. Проектирование сервера, асинхронного выполнения и общего состояния не входило в область проверки этой CLI.
В следующем упражнении расширьте программу, чтобы читать суммы из файла. Решите, как отличать ошибки файлового ввода-вывода от существующего InputError, чтобы лучше понять, как владение и типы ошибок выражают границы программы.
Обсуждение