Топ ошибок начинающих Rust разработчиков: продвинутый уровень

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

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

Начать курс

Топ ошибок начинающих Rust разработчиков: продвинутый уровень

Rust — язык, который обещает безопасную работу с памятью и высокую производительность. Однако, когда вы переходите от базовых примеров к реальным проектам, начинают всплывать ошибки, которые не решаются простым добавлением & или mut. В этой статье мы разберём четыре продвинутые ловушки, в которые часто попадают разработчики, уже освоившие основы borrow checker'а и владения.

Статья рассчитана на тех, кто уже написал несколько проектов на Rust и сталкивался с Rc, RefCell, трейтами и unsafe. Мы не будем объяснять, что такое заимствование, а сосредоточимся на скрытых граблях.

1. Утечки памяти через циклические ссылки в Rc/RefCell

Самая коварная ошибка — думать, что Rc автоматически предотвращает утечки. На самом деле, Rc использует подсчёт ссылок, и если два объекта ссылаются друг на друга, счётчик никогда не обнулится. Память не освободится.

Рассмотрим классический пример с графом:

use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    children: Vec<Rc<RefCell<Node>>>,
}

fn main() {
    let a = Rc::new(RefCell::new(Node { value: 1, children: vec![] }));
    let b = Rc::new(RefCell::new(Node { value: 2, children: vec![] }));

    // Создаём циклическую ссылку
    a.borrow_mut().children.push(Rc::clone(&b));
    b.borrow_mut().children.push(Rc::clone(&a));

    // После выхода из main память не освободится!
    drop(a);
    drop(b); // Утечка: Rc считает, что есть ещё ссылки
}

Почему это происходит? После drop(a) счётчик ссылок a уменьшается, но b всё ещё содержит ссылку на a через children. Аналогично для b. В итоге оба объекта остаются в памяти.

Решение: Используйте Weak<T> для слабых ссылок. В графах обычно один узел является владельцем (сильная ссылка), а остальные — слабые.

use std::rc::{Rc, Weak};
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>, // слабая ссылка на родителя
    children: RefCell<Vec<Rc<Node>>>, // сильные ссылки на детей
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);
    // Теперь при удалении branch leaf тоже освободится
}

2. Неправильное использование Pin для self-referential структур

Структуры, которые содержат ссылки на самих себя (self-referential), — частая причина неопределённого поведения. Rust не позволяет их создавать напрямую, но через Pin и unsafe это возможно. Ошибка — забыть, что Pin гарантирует только то, что объект не будет перемещён в памяти, но не защищает от внутренних сдвигов.

Пример с будущим (future):

use std::pin::Pin;
use std::marker::PhantomPinned;

#[derive(Debug)]
struct SelfReferential {
    data: String,
    pointer: *const String, // сырой указатель на data
    _pin: PhantomPinned,
}

impl SelfReferential {
    fn new(data: String) -> Self {
        SelfReferential {
            pointer: std::ptr::null(),
            data,
            _pin: PhantomPinned,
        }
    }

    fn init(self: Pin<&mut Self>) {
        let this = unsafe { self.get_unchecked_mut() };
        this.pointer = &this.data as *const String;
    }
}

fn main() {
    let mut value = SelfReferential::new("hello".to_string());
    let mut pinned = unsafe { Pin::new_unchecked(&mut value) };
    pinned.as_mut().init();

    // Ошибка: перемещаем value, хотя оно закреплено
    let moved = value; // <-- Это UB! Pin не защищает от move после init
    println!("{:?}", moved);
}

В чём ошибка? После вызова init указатель pointer указывает на value.data. Но когда мы перемещаем value в moved, data физически переезжает в новое место, а pointer остаётся старым — висячий указатель.

Правильный подход: Создавайте Pin сразу на куче через Box::pin или используйте pin! макрос из tokio/futures. Никогда не перемещайте Pin-объект после инициализации.

use std::pin::Pin;
use std::marker::PhantomPinned;

fn main() {
    let value = SelfReferential::new("hello".to_string());
    let mut pinned = Box::pin(value); // Закрепляем в куче
    pinned.as_mut().init();
    // Теперь перемещение невозможно — компилятор не даст
    // let moved = *pinned; // Ошибка: не реализован трейт Unpin
}

3. Перегрузка трейтов и конфликт реализации

Rust позволяет реализовать трейт для любого типа, но есть правило сироты (orphan rule): вы не можете реализовать внешний трейт для внешнего типа. Однако, когда вы начинаете писать обобщённый код, легко создать конфликт реализаций.

Типичная ошибка — попытка реализовать трейт From<T> для T и одновременно для &T:

use std::convert::From;

// Пытаемся реализовать From для обоих вариантов
impl<T> From<T> for MyWrapper<T> {
    fn from(t: T) -> Self {
        MyWrapper(t                

Блоги

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