Чистый код на Rust: принципы, идиомы и практические примеры

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: почему чистый код в Rust — это не просто стиль

Rust — язык, который сочетает производительность C++ с безопасностью памяти, но его уникальная система владения (ownership) и заимствования (borrowing) требует особого подхода к написанию кода. Принципы Clean Code, сформулированные Робертом Мартином, в Rust приобретают новое измерение: здесь чистота кода напрямую связана с корректностью, производительностью и удобством поддержки.

В этой статье мы разберём, как применять идиомы Rust для создания самодокументируемого, безопасного и эффективного кода. Вы увидите, как заменить типичные ошибки новичков на элегантные решения, принятые в сообществе Rust.

1. Идиоматическая обработка ошибок: от unwrap() к выразительным паттернам

Одна из первых проблем, с которой сталкиваются новички — злоупотребление unwrap(). В Clean Code ошибка — это не исключение, а часть архитектуры. Rust предоставляет мощные типы Option и Result для явной обработки граничных случаев.

Плохой пример (грязный код)

fn parse_number(input: &str) -> i32 {
    input.parse::<i32>().unwrap()
}

fn main() {
    let val = parse_number("не число");
    println!("{}", val); // Паника!
}

Хороший пример (чистый код)

use std::num::ParseIntError;

fn parse_number(input: &str) -> Result<i32, ParseIntError> {
    input.parse::<i32>()
}

fn main() {
    match parse_number("42") {
        Ok(n) => println!("Число: {}", n),
        Err(e) => eprintln!("Ошибка: {}", e),
    }
}

Почему это Clean Code? Функция явно сообщает, что может завершиться ошибкой. Вызывающий код сам решает, как обрабатывать сбой — логировать, повторять попытку или передавать ошибку выше.

Продвинутый приём: оператор ?

fn read_config(path: &str) -> Result<String, std::io::Error> {
    let content = std::fs::read_to_string(path)?;
    Ok(content)
}

Оператор ? — это элегантная замена громоздким match-выражениям. Он возвращает ошибку наверх, если результат — Err, и извлекает значение, если Ok. Это уменьшает вложенность и улучшает читаемость.

2. Владение и заимствование: проектируйте архитектуру, а не боритесь с компилятором

Система владения в Rust может казаться ограничением, но на самом деле она помогает писать чистый код. Проблемы начинаются, когда разработчик пытается «обмануть» компилятор с помощью clone() или Rc<RefCell> без необходимости.

Антипаттерн: избыточное клонирование

fn process_data(data: Vec<String>) {
    for item in data.clone() { // Зачем клонировать?
        println!("{}", item);
    }
    // data больше не используется
}

Чистое решение: заимствование по ссылке

fn process_data(data: &[String]) {
    for item in data {
        println!("{}", item);
    }
}

fn main() {
    let my_data = vec!["a".to_string(), "b".to_string()];
    process_data(&my_data); // Владение остаётся у main
    println!("Вектор всё ещё доступен: {:?}", my_data);
}

Принцип Clean Code: минимальная область видимости мутаций. Если функция не должна изменять данные — принимайте неизменяемую ссылку &T. Если нужно изменить — &mut T. Это делает код предсказуемым и безопасным.

Идиома: передача владения для трансформации

fn to_uppercase(mut text: String) -> String {
    text.make_ascii_uppercase();
    text // Владение возвращается
}

Здесь функция «поглощает» строку, модифицирует её и возвращает обратно. Это эффективно (без копирования) и явно: вызывающий понимает, что исходная переменная больше недействительна.

3. Замыкания и функциональный стиль: выразительность без потери производительности

Rust поддерживает замыкания (closures) и итераторы, которые позволяют писать декларативный код вместо императивных циклов. Это напрямую соответствует принципу Clean Code «одна функция — одна ответственность».

Императивный стиль (грязно)

fn find_even_squares(numbers: &[i32]) -> Vec<i32> {
    let mut result = Vec::new();
    for &n in numbers {
        if n % 2 == 0 {
            result.push(n * n);
        }
    }
    result
}

Функциональный стиль (чисто)

fn find_even_squares(numbers: &[i32]) -> Vec<i32> {
    numbers
        .iter()
        .filter(|&&n| n % 2 == 0)
        .map(|&n| n * n)
        .collect()
}

Почему это лучше? Цепочка вызовов iter() → filter() → map() читается как описание задачи: «взять числа, оставить чётные, возвести в квадрат, собрать». Нет временных переменных, нет мутации, легко добавлять новые этапы обработки.

Чистые функции и замыкания

fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y
}

fn main() {
    let add_five = make_adder(5);
    let result = add_five(10);
    println!("5 + 10 = {}", result); // 15
}

Ключевое слово move передаёт владение переменной x в замыкание — это безопасно и предсказуемо. Замыкания в Rust — это синтаксический сахар для структур с трейтами Fn/FnMut/FnOnce, что делает их полноценными объектами.

4. Структуры и трейты: проектирование по принципу SOLID

Clean Code на Rust невозможно представить без грамотного использования трейтов (traits) — аналога интерфейсов в других языках. Трейты позволяют реализовать полиморфизм без наследования и вир

Блоги

Рекомендаций по книгам