10 фатальных ошибок новичков в Rust и как их избежать

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

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

Начать курс

Почему 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.

Блоги

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