Привет, Хабр!
Сегодня рассмотрим проблемную тему в 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 помогает писать безопасный и предсказуемый код даже в самых хитрых случаях.
Также все желающие могут ознакомиться с другими нашими открытыми уроками в календаре мероприятий.
ссылка на оригинал статьи https://habr.com/ru/articles/905786/
Добавить комментарий