Топ ошибок начинающих 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