Привет, Хабр!
Сегодня рассмотрим проблемную тему в Rust: управление владением в структурах с циклическими ссылками, таких как графы и деревья. Особое внимание уделим комбинации Rc<RefCell<T>>
и тому, как избежать зацикливания с помощью Weak
.
Проблема: зацикливание владения
На простом: есть два объекта. Один ссылается на другой. Второй — на первый. Оба используют Rc
. Всё красиво… пока ты не понимаешь, что их strong_count
никогда не падает до нуля.
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
if let Cons(_, ref tail) = *a {
*tail.borrow_mut() = Rc::clone(&b);
}
// println!("{:?}", a); // Стек в огне ?
}
Что происходит: a
держит b
, b
держит a
.Никакой drop не произойдёт. Никогда. Даже Drop
‑трейты не вызовутся.
Weak как решение
Rust предлагает выход — Weak<T>
. Это ссылка без права собственности. Она не увеличивает strong_count
, а значит, не участвует в цикле владения. Но её надо апгрейдить вручную, и там может быть None
.
Переделаем структуру:
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
next: RefCell<Option<Rc<Node>>>,
prev: RefCell<Option<Weak<Node>>>,
}
fn main() {
let first = Rc::new(Node {
value: 1,
next: RefCell::new(None),
prev: RefCell::new(None),
});
let second = Rc::new(Node {
value: 2,
next: RefCell::new(None),
prev: RefCell::new(Some(Rc::downgrade(&first))),
});
*first.next.borrow_mut() = Some(Rc::clone(&second));
println!("first strong = {}, weak = {}", Rc::strong_count(&first), Rc::weak_count(&first));
}
Теперь prev
— слабая ссылка. Цикла нет. Память освободится, когда оба узла выйдут из скоупа.
Сложнее: дерево с родителями и детьми
Кейс: UI‑компоненты, AST, DOM — везде есть родитель и дети. Хотим, чтобы узел знал о своём родителе. Но если мы сделаем Rc
и туда, и туда — словим цикл.
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct TreeNode {
value: i32,
parent: RefCell<Weak<TreeNode>>,
children: RefCell<Vec<Rc<TreeNode>>>,
}
fn main() {
let root = Rc::new(TreeNode {
value: 0,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
let child = Rc::new(TreeNode {
value: 1,
parent: RefCell::new(Rc::downgrade(&root)),
children: RefCell::new(vec![]),
});
root.children.borrow_mut().push(Rc::clone(&child));
if let Some(parent) = child.parent.borrow().upgrade() {
println!("Родитель: {}", parent.value);
}
}
Такой паттерн — стандарт для двунаправленных структур.
AST и интерпретаторы
Деревья выражений — самый частый случай. Хочется, чтобы Expr
знал, кто его родитель. И тут Weak
:
struct Expr {
kind: ExprKind,
parent: RefCell<Weak<Stmt>>,
}
enum ExprKind {
BinaryOp {
left: Rc<Expr>,
op: String,
right: Rc<Expr>,
},
Identifier(String),
}
struct Stmt {
expr: Rc<Expr>,
}
Если у тебя язык с снизу‑вверх семантикой, тебе точно пригодится .upgrade()
родителя для анализа контекста.
Тестирование и дебаг
Как не попасться на утечку:
Проверяй счетчики
println!(
"strong: {}, weak: {}",
Rc::strong_count(&node),
Rc::weak_count(&node)
);
Если strong > 1
и ты не знаешь почему — беда близко.
Оборачивай всё в Drop
Добавь Drop
на структуры, и в него — println!
. Если не сработал — утечка.
try_borrow вместо borrow
Потому что borrow_mut
может паникнуть.
if let Ok(mut data) = self.inner.try_borrow_mut() {
// Всё ок
}
Дебаг через miri
cargo +nightly miri run
Покажет aliasing, подвешенные ссылки и утечки.
Контраргументы
А может ну его? Пусть утечёт, всё равно GC нет…
Можно. Если ты пишешь утилиту, которая живёт 10 секунд. Но в играх, GUI, DSL‑интерпретаторах это ведёт к:
Утечкам на каждый клик/выражение
Объектам, которые нельзя удалить
Росту памяти со временем
Обязательно нужно освобождать память. Иначе ты пишешь на C, только без free
.
Вывод
Ты не избежишь Rc<RefCell<T>>
в Rust, если строишь сложные структуры. Но ты можешь победить их. Используй Weak
там, где нет логического владения. Делай .upgrade()
и обрабатывай None
. Следи за strong_count
. И думай как архитектор.
Всех тех, кто хочет глубже понять, почему Rust — это не просто модный язык, а настоящий инструмент архитекторов кода, мы приглашаем на открытый урок 22 мая «За что разработчики любят Rust» — разберём, как Rust помогает писать безопасный и предсказуемый код даже в самых хитрых случаях.
Также все желающие могут ознакомиться с другими нашими открытыми уроками в календаре мероприятий.
Cerberuser
https://www.youtube.com/watch?v=Ok1-XYV3k60