Почему Rust ломает шаблоны (и это нормально)
Rust — это язык-вызов. Он не прощает лени, не даёт спуску на «и так сойдёт» и требует от программиста глубокого понимания памяти. Именно поэтому многие разработчики, особенно после Python или JavaScript, испытывают культурный шок. Вы привыкли, что интерпретатор сам подчищает за вами мусор? Забудьте. Rust говорит: «Ты хозяин своей памяти, но за каждую ошибку заплатишь компиляцией».
В этой статье мы разберём 10 главных ошибок при изучении Rust, которые совершают новички. Мы не просто перечислим их — мы покажем, как их исправить с помощью реального кода. Если вы только начинаете свой путь в Rust или уже набили несколько шишек — этот материал для вас.
Ошибка №1: Игнорирование borrow checker'а
Самая частая жалоба: «Почему компилятор ругается на всё?!». Borrow checker — это не враг, а ваш лучший друг. Он не даёт вам выстрелить себе в ногу. Проблема в том, что новички пытаются обойти его, а не понять логику.
Как выглядит типичная ошибка
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s; // ОШИБКА!
println!("{}, {}", r1, r2);
}
Компилятор выдаст: cannot borrow `s` as mutable because it is also borrowed as immutable. Новичок в панике удаляет mut или начинает плодить clone(). Правильное решение — сузить область видимости заимствований.
Исправление
fn main() {
let mut s = String::from("hello");
let r1 = &s;
println!("{}", r1); // immutable borrow заканчивается здесь
let r2 = &mut s;
println!("{}", r2);
}
Совет: Воспринимайте borrow checker как ревнивого партнёра — он разрешает только одно изменяемое заимствование или сколько угодно неизменяемых, но не одновременно.
Ошибка №2: Страх перед lifetimes (временами жизни)
Lifetimes пугают новичков больше всего. Синтаксис 'a выглядит как магия, но на самом деле это просто аннотации для borrow checker, которые говорят: «Эта ссылка живёт столько же, сколько та».
Проблема: возврат ссылки из функции
fn longest(x: &str, y: &str) -> &str { // ОШИБКА: missing lifetime specifier
if x.len() > y.len() { x } else { y }
}
Компилятор не знает, какая из ссылок будет возвращена. Решение — явно указать, что обе ссылки живут одинаково:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
Лайфхак: В 90% случаев lifetimes можно опустить — компилятор сам их выведет (elision). Если просит — просто добавьте 'a и не паникуйте.
Ошибка №3: Чрезмерное использование clone()
Когда borrow checker не пускает, новички делают .clone(). Это работает, но убивает главное преимущество Rust — zero-cost abstractions. Код начинает тормозить и жрать память.
Плохой пример
let s1 = String::from("hello");
let s2 = s1.clone(); // зачем?!
println!("{}", s1); // работает только из-за clone
Лучше использовать заимствование или перемещение:
let s1 = String::from("hello");
let s2 = &s1; // заимствование
println!("{}", s1); // всё ок
Золотое правило: clone() используйте только для сложных структур данных, где заимствование невозможно (например, при передаче в другой поток).
Ошибка №4: Непонимание владения (ownership)
Rust — единственный язык, где владение — это не философия, а правило компиляции. Каждое значение имеет ровно одного владельца. Когда владелец выходит из области видимости — значение удаляется.
Классическая ошибка
let s = String::from("hello");
let t = s; // перемещение
println!("{}", s); // ОШИБКА: value borrowed here after move
Решение: либо let t = s.clone(), либо let t = &s (заимствование).
Мнемоника: Представьте, что переменная — это рука, держащая чашку. Перемещение — вы отдали чашку другому. Ваша рука теперь пуста.
Ошибка №5: Игнорирование Result и Option
В Python вы привыкли к исключениям и None. В Rust нет исключений — есть Result и Option. Новички часто пишут .unwrap() везде, что приводит к панике при ошибке.
Плохой код
let file = File::open("test.txt").unwrap(); // краш, если файла нет
Правильный подход
let file = match File::open("test.txt") {
Ok(f) => f,
Err(e) => {
eprintln!("Ошибка: {}", e);
return;
}
};
Или используйте ? оператор для проброса ошибки:
fn read_file() -> Result<String, io::Error> {
let mut file = File::open("test.txt")?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
Важно: unwrap() — это костыль для прототипов. В production используйте match или ?.
Ошибка №6: Путаница между String и &str
В Rust два типа строк: String (owned, изменяемая, в куче) и &str (ссылка на строку, неизменяемая). Новички не понимают, когда что использовать.
Типичная ошибка
fn greet(name: String) {
println!("Hello {}", name);
}
let name = "Alice"; // &str
greet(name); // ОШИБКА: expected String, found &str
Правило: Если функция только читает строку — принимайте &str. Если нужно сохранить или изменить — String.