Rust: как не утечь в Rc<RefCell

от автора

Привет, Хабр!

Сегодня рассмотрим проблемную тему в 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *