Почему чистый код на 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(|&&