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

Сегодня рассмотрим проблемную тему в 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 помогает писать безопасный и предсказуемый код даже в самых хитрых случаях.

Также все желающие могут ознакомиться с другими нашими открытыми уроками в календаре мероприятий.

Комментарии (1)


  1. Cerberuser
    22.05.2025 08:36

    Как не утечь в Rc

    https://www.youtube.com/watch?v=Ok1-XYV3k60