Перейти к основному содержимому

Рекомендации по разработке на Rust

Разработчику Архитектору

Рекомендации по разработке на Rust​

Введение в культуру кода Rust​

Rust формирует особую культуру разработки, основанную на строгой системе типов, гарантиях безопасности памяти без сборщика мусора и нулевых издержках абстракций. Эти принципы влияют на каждый аспект написания кода — от именования элементов до архитектурных решений. Культура кода в экосистеме Rust развивалась сообществом через годы обсуждений в репозиториях, на форумах и в официальных руководствах. Следование этим практикам упрощает чтение чужого кода, снижает количество ошибок и повышает производительность программ.

Ключевые ценности идиоматичного Rust кода включают явное управление владением ресурсами, предпочтение компиляторной проверки времени выполнения, использование трейтов для полиморфизма вместо наследования и обработку ошибок через типы Result и Option. Эти подходы формируют мышление разработчика, ориентированное на предотвращение классических проблем системного программирования ещё на этапе компиляции.


Соглашения об именовании​

Основные правила именования​

Rust следует единому стилю именования, определённому в официальном руководстве по стилю кода (Rust API Guidelines). Каждый элемент языка использует строго определённую нотацию:

Элемент языкаНотацияПример
Модули, крейтыsnake_casenetwork_utils
Типы (структуры, перечисления)PascalCaseHttpRequest, StatusCode
ТрейтыPascalCaseDisplay, Iterator
Функции, методыsnake_caseprocess_data, to_string
Переменные, параметрыsnake_casebuffer_size, max_retries
КонстантыSCREAMING_SNAKE_CASEMAX_BUFFER_SIZE
Статические переменныеSCREAMING_SNAKE_CASEGLOBAL_CONFIG
Жизненные циклыстрочные буквы'a, 'static
Обобщённые параметрыPascalCaseT, Item, ErrorType

Имена должны точно отражать назначение элемента. Для логических переменных и функций, возвращающих bool, используйте префиксы is_, has_, can_, should_. Например — is_valid, has_permission, can_connect. Избегайте избыточных префиксов и суффиксов, таких как flag_ или _bool.


Именование перечислений и вариантов​

Перечисления именуются в PascalCase, варианты перечислений также используют PascalCase. Для перечислений, представляющих ошибки, применяется суффикс Error. Для перечислений состояний — суффикс State.

enum ConnectionError {
Timeout,
Refused,
InvalidCertificate,
}

enum ProcessingState {
Idle,
Running,
Completed,
}

Именование методов​

Методы, изменяющие состояние объекта, именуются глаголами в повелительном наклонении — push, pop, clear. Методы преобразования используют префикс to_ для неизменяемых преобразований и into_ для преобразований с потреблением исходного значения:

let s = String::from("hello");
let bytes = s.into_bytes(); // s больше не существует

let s2 = String::from("world");
let chars: Vec<char> = s2.chars().collect(); // s2 остаётся доступным

Методы, возвращающие ссылку на внутреннее состояние, используют префикс as_: as_str, as_slice. Методы, возвращающие итератор, заканчиваются на _iter или _into_iter.


Форматирование кода​

Использование rustfmt​

Форматирование кода в экосистеме Rust стандартизировано через инструмент rustfmt. Этот инструмент применяет единые правила отступов, пробелов и разрывов строк. Рекомендуется настроить автоматическое форматирование при сохранении файла в редакторе или перед коммитом через хуки Git.

Конфигурация форматирования хранится в файле rustfmt.toml в корне проекта. Базовая конфигурация включает:

edition = "2021"
max_width = 100
hard_tabs = false
tab_spaces = 4
newline_style = "Unix"
use_small_heuristics = "Max"

Отступы и пробелы​

Используйте четыре пробела для каждого уровня вложенности. Не применяйте символы табуляции. После ключевых слов if, while, for, match ставится пробел перед открывающей скобкой. Операторы сравнения и арифметические операторы окружены пробелами с обеих сторон.

// Правильно
if x > 10 && y < 20 {
process_value(x + y);
}

// Неправильно
if(x>10&&y<20){
process_value(x+y);
}

Расположение фигурных скобок​

Фигурные скобки размещаются на той же строке, что и управляющая конструкция. Исключение составляют объявления функций и типов — открывающая скобка размещается на следующей строке при многострочном объявлении.

// Для управляющих конструкций
if condition {
do_something();
}

// Для функций с длинной сигнатурой
fn process_large_dataset(
data: &[u8],
config: &ProcessingConfig,
callback: impl FnMut(&[u8]),
) -> Result<(), ProcessingError> {
// тело функции
}

Вертикальное выравнивание​

Для улучшения читаемости многострочных выражений применяйте вертикальное выравнивание. При вызове функций с множеством аргументов каждый аргумент размещается на отдельной строке с выравниванием:

let result = calculate_metrics(
input_data,
processing_parameters,
output_format,
error_handling_strategy,
);

При цепочках методов точка размещается в начале строки перед вызовом метода:

let filtered = data
.iter()
.filter(|x| x.is_valid())
.map(|x| x.normalize())
.collect::<Vec<_>>();

Структура проекта и организация модулей​

Иерархия модулей​

Проекты на Rust организуются через систему модулей. Корневой модуль определяется в файле src/lib.rs для библиотек или src/main.rs для бинарных приложений. Для крупных проектов применяется многофайловая структура модулей:

src/
├── lib.rs // точка входа в библиотеку
├── network/
│ ├── mod.rs // объявление модуля network
│ ├── tcp.rs
│ ├── tls.rs
│ └── http.rs
├── storage/
│ ├── mod.rs
│ ├── file.rs
│ └── memory.rs
└── utils/
├── mod.rs
└── math.rs

Файл mod.rs в каждой директории объявляет содержимое модуля и экспортирует публичные элементы:

// src/network/mod.rs
mod tcp;
mod tls;
mod http;

pub use tcp::TcpConnection;
pub use tls::TlsStream;
pub use http::{HttpRequest, HttpResponse};

Разделение интерфейса и реализации​

Публичный интерфейс модуля определяется через ключевое слово pub. Внутренние детали реализации остаются приватными. Для экспорта элементов из вложенных модулей используйте цепочку pub use в родительских модулях. Это создаёт чистый и стабильный интерфейс для внешних пользователей крейта.

// src/lib.rs
pub mod network;
pub mod storage;

// Внешний код использует: my_crate::network::TcpConnection

Организация тестов​

Тесты размещаются в модуле tests внутри файла или в отдельном файле tests/. Для unit-тестов применяется встроенный модуль #[cfg(test)]:

// src/network/tcp.rs
pub struct TcpConnection { /* ... */ }

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_connection_establishment() {
// тестовый код
}
}

Интеграционные тесты размещаются в директории tests/ на уровне проекта:

my_project/
├── src/
└── tests/
├── network_tests.rs
└── storage_tests.rs

Проектирование типов и трейтов​

Принцип единственной ответственности​

Каждый тип должен решать одну конкретную задачу. Структуры, объединяющие несвязанные данные, усложняют поддержку и тестирование. Разделяйте ответственности на отдельные типы с чёткими границами.

Код ITЗагрузка примера кода…


Использование новых типов​

Паттерн новых типов (newtype pattern) повышает типобезопасность, оборачивая примитивные типы в структуры с семантическим значением:

struct UserId(u64);
struct EmailAddress(String);

fn send_notification(user_id: UserId, email: EmailAddress) {
// компилятор предотвращает передачу обычного u64 вместо UserId
}

Этот подход предотвращает ошибки смешения значений одинакового типа, но разного назначения.


Проектирование трейтов​

Трейты определяют поведение, а не состояние. Избегайте трейтов с методами, изменяющими внутреннее состояние без чёткой семантики. Предпочитайте композицию трейтов наследованию.

Для расширяемости проектируйте трейты с минимальным набором обязательных методов. Остальные методы реализуйте как предоставленные (provided methods) с дефолтной реализацией:

trait Storage {
// Обязательный метод
fn save(&mut self, key: &str, value: &[u8]) -> Result<(), StorageError>;

// Предоставленный метод с дефолтной реализацией
fn load_string(&self, key: &str) -> Result<String, StorageError> {
let bytes = self.load(key)?;
String::from_utf8(bytes).map_err(|e| StorageError::InvalidData(e))
}

// Другой обязательный метод
fn load(&self, key: &str) -> Result<Vec<u8>, StorageError>;
}

Обобщённое программирование​

Используйте обобщённые параметры для написания переиспользуемого кода. Ограничивайте обобщения трейтами вместо конкретных типов:

// Плохо: привязка к конкретному типу
fn process_data(data: Vec<i32>) { /* ... */ }

// Хорошо: обобщение с трейтом
fn process_data<T: Display + Clone>(data: Vec<T>) { /* ... */ }

Для сложных сценариев применяйте трейт-объекты через dyn Trait, когда обобщения не подходят из-за ограничений времени компиляции или необходимости динамической диспетчеризации.


Владение и заимствование в проектировании​

Правила передачи владения​

Функции принимают параметры по значению, когда им требуется владение ресурсом. Для чтения данных используйте заимствование через &T. Для изменения данных — через &mut T. Избегайте ненужного клонирования данных.

Код ITЗагрузка примера кода…


Возврат владения​

Функции возвращают владение через тип возврата. Для возврата ссылки на внутреннее состояние применяйте аннотации жизненного цикла:

Код ITЗагрузка примера кода…


Избегание лишних копий​

Для типов, реализующих Copy, копирование происходит автоматически и бесплатно. Для остальных типов применяйте clone() осознанно. Предпочитайте заимствование перед клонированием:

// Плохо: ненужное клонирование
fn process_user(user: User) {
let name = user.name.clone();
println!("{}", name);
// user больше не доступен
}

// Хорошо: заимствование
fn process_user(user: &User) {
println!("{}", user.name);
// user остаётся доступным
}

Обработка ошибок​

Использование типов Result и Option​

Rust использует типы Result<T, E> и Option<T> для явного представления возможных ошибок и отсутствующих значений. Избегайте паник в обычном потоке выполнения. Паники допустимы только в ситуациях, которые представляют внутреннюю ошибку программы, которую невозможно корректно обработать.

// Плохо: паника при обработке пользовательского ввода
fn parse_number(input: &str) -> i32 {
input.parse().expect("Invalid number")
}

// Хорошо: возврат результата с ошибкой
fn parse_number(input: &str) -> Result<i32, ParseIntError> {
input.parse()
}

Создание кастомных типов ошибок​

Для сложных приложений создавайте иерархию типов ошибок, реализующих трейт std::error::Error. Используйте библиотеку thiserror для упрощения создания ошибок:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum ProcessingError {
#[error("Invalid input format: {0}")]
InvalidFormat(String),

#[error("IO error: {0}")]
Io(#[from] std::io::Error),

#[error("Network timeout after {0} seconds")]
Timeout(u64),
}

Композиция ошибок через вопросительный оператор​

Вместо вложенных блоков match используйте оператор ? для пробрасывания ошибок. Это упрощает чтение кода и делает поток управления линейным:

fn process_file(path: &Path) -> Result<(), ProcessingError> {
let content = fs::read_to_string(path)?;
let data = parse_content(&content)?;
validate_data(&data)?;
save_processed_data(&data)?;
Ok(())
}

Обработка цепочек ошибок​

Для сохранения контекста ошибок применяйте методы context из библиотеки anyhow или создавайте обёртки вручную. Это помогает при диагностике проблем в продакшене:

use anyhow::{Context, Result};

fn load_config(path: &Path) -> Result<Config> {
let content = fs::read_to_string(path)
.context("Failed to read config file")?;
let config = serde_json::from_str(&content)
.context("Failed to parse config JSON")?;
Ok(config)
}

Асинхронное программирование​

Выбор асинхронного рантайма​

Rust поддерживает несколько асинхронных рантаймов — tokio, async-std, smol. Для большинства приложений рекомендуется tokio из-за зрелости экосистемы и производительности. Указывайте рантайм явно через атрибуты или макросы:

#[tokio::main]
async fn main() {
// асинхронный код
}

Структурирование асинхронного кода​

Асинхронные функции помечаются ключевым словом async. Для ожидания завершения задачи используется .await. Избегайте блокирующих операций внутри асинхронных функций — используйте асинхронные альтернативы или выносите блокирующий код в отдельные потоки через tokio::task::spawn_blocking.

async fn fetch_data(url: &str) -> Result<Vec<u8>, reqwest::Error> {
let response = reqwest::get(url).await?;
response.bytes().await
}

Управление конкурентностью​

Для параллельного выполнения задач используйте tokio::spawn или futures::future::join_all. Для ограничения параллелизма применяйте семафоры или пулы задач:

Код ITЗагрузка примера кода…


Документирование кода​

Структура документационных комментариев​

Документация в Rust пишется в специальных комментариях с тройным слешем ///. Для модулей и крейтов используется комментарий //!. Документация компилируется в гипертекстовые страницы через cargo doc.

/// Представляет соединение с удалённым сервером.
///
/// Соединение устанавливается через метод `connect` и автоматически
/// закрывается при выходе из области видимости благодаря реализации трейта `Drop`.
///
/// # Примеры
///
/// ```
/// use my_crate::TcpConnection;
///
/// let mut conn = TcpConnection::connect("example.com:80")
/// .expect("Failed to connect");
/// conn.send(b"GET / HTTP/1.1\r\n\r\n").expect("Failed to send");
/// let response = conn.receive().expect("Failed to receive");
/// ```
pub struct TcpConnection {
// поля структуры
}

Элементы качественной документации​

Каждый публичный элемент должен содержать:

  • Краткое описание назначения
  • Подробное объяснение поведения и ограничений
  • Примеры использования в блоках кода
  • Описание ошибок через секцию # Ошибки
  • Описание паник через секцию # Паники (если применимо)
  • Ссылки на связанные типы и трейты

Для методов описывайте взаимодействие параметров и возвращаемых значений. Указывайте предусловия и постусловия поведения.


Тестирование документации​

Примеры кода в документации компилируются и выполняются как тесты при запуске cargo test. Это гарантирует актуальность примеров. Используйте атрибуты для управления выполнением:

/// # Примеры
///
/// Тестовый пример, который не должен выполняться:
/// ```no_run
/// let result = dangerous_operation();
/// ```
///
/// Пример для модуля:
/// ```ignore
/// // этот код не компилируется, но показывает концепцию
/// ```

Тестирование​

Структура тестов​

Разделяйте тесты на три категории:

  • Юнит-тесты — тестируют отдельные функции и методы, размещаются в модуле #[cfg(test)] внутри файла реализации
  • Интеграционные тесты — тестируют взаимодействие компонентов, размещаются в директории tests/
  • Документационные тесты — примеры в документации, проверяются через cargo test --doc

Написание эффективных тестов​

Каждый тест должен проверять одно конкретное поведение. Используйте описательные имена тестов в стиле snake_case, начинающиеся с test_:

#[test]
fn test_user_creation_with_valid_email() {
let user = User::new("test@example.com", "John");
assert!(user.is_valid());
}

#[test]
fn test_user_rejection_with_invalid_email() {
let result = User::new("invalid-email", "John");
assert!(result.is_err());
}

Для параметризованных тестов применяйте макросы или циклы внутри тестовой функции:

#[test]
fn test_email_validation() {
let valid_emails = vec![
"user@example.com",
"user.name+tag@example.co.uk",
];

for email in valid_emails {
assert!(is_valid_email(email));
}
}

Тестирование ошибок​

Для проверки ожидаемых ошибок используйте методы unwrap_err или сопоставление с образцом:

#[test]
fn test_division_by_zero() {
let result = divide(10, 0);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), MathError::DivisionByZero);
}

Мокирование зависимостей​

Для изолированного тестирования применяйте трейты вместо конкретных типов. Создавайте фиктивные реализации трейтов для тестов:

Код ITЗагрузка примера кода…


Безопасность и надёжность​

Проверка безопасности памяти​

В safe Rust компилятор отклоняет use-after-free, double-free и data races при соблюдении правил владения. Это не заменяет проверку логики, обработку panic и контракты unsafe и FFI. Типичное приложение не использует tracing GC, но имеет runtime (паника, аллокатор, в async — executor). Блоки unsafe — только при необходимости, с документированными инвариантами.


Валидация входных данных​

Все данные из внешних источников должны проходить валидацию перед использованием. Это включает:

  • Проверку границ при работе с индексами
  • Валидацию форматов строк (email, URL, даты)
  • Ограничение размеров буферов для предотвращения переполнения
  • Санитизацию пользовательского ввода перед использованием в системных вызовах
fn process_user_input(input: &str) -> Result<(), ValidationError> {
if input.len() > MAX_INPUT_LENGTH {
return Err(ValidationError::TooLong);
}

if !input.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
return Err(ValidationError::InvalidCharacters);
}

Ok(())
}

Защита от атак​

При работе с сетевыми соединениями и внешними данными применяйте:

  • Таймауты для всех операций ввода-вывода
  • Ограничение скорости запросов
  • Валидацию сертификатов TLS
  • Использование параметризованных запросов для предотвращения SQL-инъекций
  • Экранирование выходных данных для предотвращения XSS

Производительность​

Измерение производительности​

Для измерения производительности используйте бенчмарки через #[bench] или библиотеку criterion. Измеряйте реальные сценарии использования, а не микро-оптимизации.

use criterion::{criterion_group, criterion_main, Criterion};

fn benchmark_processing(c: &mut Criterion) {
let data = generate_test_data();
c.bench_function("process_large_dataset", |b| {
b.iter(|| process_data(&data))
});
}

criterion_group!(benches, benchmark_processing);
criterion_main!(benches);

Оптимизация через профилирование​

Применяйте профилировщики (perf на Linux, Instruments на macOS) для выявления узких мест. Оптимизируйте только те участки кода, которые действительно влияют на производительность. Избегайте преждевременной оптимизации.


Выбор эффективных структур данных​

Для разных сценариев используйте подходящие структуры:

  • Vec для последовательного доступа и добавления в конец
  • HashMap для быстрого поиска по ключу
  • BTreeMap для упорядоченных данных и диапазонных запросов
  • SmallVec или ArrayVec для маленьких коллекций для избежания аллокаций
  • Cow для избежания копирования при неизменяемом использовании
use smallvec::SmallVec;

// Для небольших списков до 8 элементов аллокация не происходит
let mut items: SmallVec<[Item; 8]> = SmallVec::new();

Избегание ненужных аллокаций​

Переиспользуйте буферы вместо создания новых на каждой итерации. Применяйте методы, работающие на месте (sort вместо sorted), когда это возможно. Используйте итераторы вместо промежуточных коллекций.

Код ITЗагрузка примера кода…


Инструменты и конфигурация проекта​

Конфигурация Cargo.toml​

Файл Cargo.toml определяет метаданные крейта, зависимости и настройки сборки. Структурируйте зависимости по секциям [dependencies], [dev-dependencies] и [build-dependencies]. Указывайте версии зависимостей с использованием семантического версионирования.

Код ITЗагрузка примера кода…


Интеграция с clippy​

Clippy — линтер для поиска распространённых ошибок и неидиоматичного кода. Запускайте его регулярно через cargo clippy. Настройте допустимые предупреждения в файле clippy.toml:

# clippy.toml
too-many-arguments-threshold = 8

Для подавления конкретных предупреждений используйте атрибуты:

#[allow(clippy::needless_return)]
fn example() -> i32 {
return 42; // clippy предлагает убрать return
}

Настройка среды разработки​

Рекомендуемые инструменты для разработки на Rust:

  • rust-analyzer — языковой сервер для автодополнения и навигации
  • cargo-watch — автоматическая пересборка при изменении файлов
  • cargo-expand — просмотр раскрытых макросов
  • cargo-audit — проверка уязвимостей в зависимостях
  • cargo-outdated — поиск устаревших зависимостей

Настройте редактор для автоматического форматирования через rustfmt при сохранении файла и запуска cargo check в фоновом режиме.


Практические паттерны проектирования​

Построитель (Builder)​

Паттерн построителя упрощает создание сложных объектов с множеством опциональных параметров:

Код ITЗагрузка примера кода…


Обёртка (Wrapper)​

Паттерн обёртки добавляет функциональность к существующему типу без изменения его исходного кода:

Код ITЗагрузка примера кода…


Состояние (State)​

Паттерн состояния инкапсулирует поведение, зависящее от текущего состояния объекта:

Код ITЗагрузка примера кода…

Эти паттерны помогают создавать гибкие и поддерживаемые архитектуры, соответствующие принципам языка Rust.


Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.