Чистый код на Rust: пишем элегантно и эффективно

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

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

Начать курс

Почему чистый код на Rust — это не роскошь, а необходимость

Rust — это язык, который требует от разработчика дисциплины. Компилятор здесь строгий, но справедливый: он не прощает небрежности с памятью, но даёт взамен невероятную скорость и безопасность. Однако, чтобы по-настоящему раскрыть потенциал Rust, мало просто заставить код компилироваться. Нужно писать чистый код — понятный, поддерживаемый и элегантный.

В этой статье мы разберём, как писать чистый код на Rust так, чтобы ваш проект не превращался в лабиринт из unsafe блоков и запутанных дженериков. Вы узнаете о принципах именования, идиомах работы с владением, обработке ошибок и структурировании кода. Поехали!

1. Именование: говорите на языке Rust

Правильное именование — половина успеха. В Rust есть строгие соглашения, и их нарушение воспринимается как красная тряпка для быка (особенно на code review).

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

  • snake_case для переменных, функций, методов и модулей.
  • PascalCase для типов (struct, enum, trait).
  • SCREAMING_SNAKE_CASE для констант и статических переменных.

Пример:

// Плохо
let UserName = "Alice";
fn GetUserData() {}
const MAX_SIZE: u32 = 100;

// Хорошо
let user_name = "Alice";
fn get_user_data() {}
const MAX_SIZE: u32 = 100;

Избегайте сокращений (если они не очевидны)

Вместо calc_avg() лучше написать calculate_average(). Вместо tmp — temp_buffer. Rust поощряет читаемость, а не краткость.

Совет: если вы пишете функцию, которая принимает замыкание, используйте имена f, g или closure — это общепринятая практика.

2. Владение и заимствование: дружите с компилятором

Система владения — сердце Rust. Чистый код уважает правила ownership и borrowing, не пытаясь их обойти через clone() или Rc без необходимости.

Правило: предпочитайте заимствование владению

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

// Плохо: лишний clone
fn process(data: Vec<i32>) {
    let mut sum = 0;
    for &val in &data {
        sum += val;
    }
    println!("Sum: {}", sum);
}

// Хорошо: заимствование
fn process(data: &[i32]) {
    let mut sum = 0;
    for &val in data {
        sum += val;
    }
    println!("Sum: {}", sum);
}

Избегайте избыточного клонирования

Если вам нужно передать данные в несколько мест, используйте Rc (для однопоточных сценариев) или Arc (для многопоточных). Но не злоупотребляйте: для простых случаев достаточно ссылок.

3. Обработка ошибок: откажитесь от паники

В Rust есть два основных способа обработки ошибок: panic! (неустранимая ошибка) и Result<T, E> (восстановимая). Чистый код использует Result везде, где это возможно. Паника — это последнее средство для truly exceptional ситуаций (например, деление на ноль).

Используйте оператор ?

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

use std::fs::File;
use std::io::Read;

fn read_file(path: &str) -> Result<String, std::io::Error> {
    let mut file = File::open(path)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

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

Вместо того чтобы использовать String как ошибку, определите перечисление:

#[derive(Debug)]
enum MyError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
}

impl From<std::io::Error> for MyError {
    fn from(err: std::io::Error) -> MyError {
        MyError::Io(err)
    }
}

impl From<std::num::ParseIntError> for MyError {
    fn from(err: std::num::ParseIntError) -> MyError {
        MyError::Parse(err)
    }
}

Теперь вы можете использовать ? для разных типов ошибок, и они будут автоматически конвертироваться в ваш кастомный тип.

4. Структуры данных: проектируйте с умом

Чистый код на Rust предполагает, что вы выбираете правильную структуру данных для задачи. Не пытайтесь сделать всё через Vec и HashMap — иногда BTreeMap или LinkedList подходят лучше.

Используйте newtype-паттерн

Если вам нужно отличить один тип от другого (например, ID пользователя от ID заказа), оберните базовый тип в кортежную структуру:

struct UserId(u64);
struct OrderId(u64);

fn get_user(user_id: UserId) { /* ... */ }
fn get_order(order_id: OrderId) { /* ... */ }

// Компилятор теперь не даст перепутать:
get_user(UserId(42));
// get_user(OrderId(42)); // Ошибка!

Предпочитайте enums вместо множества структур

Если у вас есть несколько вариантов одного понятия, используйте перечисление:

enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle { base: f64, height: f64 },
}

fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
        Shape::Rectangle { width, height } => width * height,
        Shape::Triangle { base, height } => 0.5 * base * height,
    }
}

Это избавляет от кучи if-else и делает код более выразительным.

5. Идиомы Rust: пишите как местный

В Rust есть свои «повадки», которые отличают чистый код от просто работающего. Вот несколько ключевых идиом:

Итераторы вместо циклов

Вместо ручного for с индексами используйте цепочки итераторов:

// Плохо
let mut result = Vec::new();
for i in 0..data.len() {
    if data[i] % 2 == 0 {
        result.push(data[i] * 2);
    }
}

// Хорошо
let result: Vec<_> = data.iter()
    .filter(|&&                

Блоги

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