Перейти к основному содержимому
  1. Rust/

Rc в Rust: Разделённое владение, циклические ссылки и Weak

·1505 слов·8 минут· loading · loading · · ·Rust-middle
О Rust - Эта статья часть цикла.
Статей прочитано 0/38
0%
📚 Введение и дополнительные материалы
🟢 Начальный уровень (Rust-basic)
Не прочитана
🔵 Средний уровень (Rust-middle)
30 Rc в Rust: Разделённое владение, циклические ссылки и Weak<T> (текущая)
Не прочитана

Введение
#

Представьте ситуацию: у вас есть данные в куче (heap), и вы хотите, чтобы несколько независимых объектов могли одновременно ими пользоваться. В обычных языках программирования ссылки раздаются направо и налево, но в Rust строгая система владения требует четкого понимания: кто является единственным владельцем ресурса?

Если владельцев должно быть несколько одновременно (в пределах одного потока), на сцену выходит умный указатель Rc<T> (Reference Counted).


1. Что такое Rc и как работает strong_count
#

Rc<T> — это управляющий указатель с подсчетом ссылок для однопоточных сценариев.

Каждый раз, когда вы делаете Rc::clone(&a), вы не копируете сами данные внутри кучи. Вместо этого вы просто увеличиваете служебный счетчик сильных ссылок — strong_count — на 1. А когда очередная переменная выходит из области видимости, drop() уменьшает strong_count на 1.

Как только strong_count становится равен 0, память кучи автоматически очищается!

Примечание

Почему бы не использовать обычную ссылку &T? &T — это временное заимствование. Ссылка жестко ограничена временем жизни (lifetimes), её нельзя просто так сохранить в структуре без явной аннотации 'a. Rc<T> решает эту проблему, давая независимое разделяемое владение.

Пример разделения данных между структурами
#

Представьте, что у нас есть описание города City, которое одновременно используется в справочнике туриста и в отчете транспортной компании:

Пример c городами и историей
Шаг 1/2
 1// ?compile_fail
 2#[derive(Debug)]
 3struct City {
 4    name: String,
 5    history: String,
 6}
 7
 8#[derive(Debug)]
 9struct CitySummary {
10    headline: String,
11    description: String,
12}
13
14fn main() {
15    let history = String::from("Город основан в 1574 году");
16
17    let city = City {
18        name: String::from("Уфа"),
19        history: history,
20    };
21
22    let summary = CitySummary {
23        headline: String::from("Уфа"),
24        description: history, // ❌ перемещено из-под city
25    };
26
27    println!("{:?}, {:?}", city, summary);
28}

1. Ошибка: попытка поделиться строкой напрямую между структурами

  • Мы либо копируем строку, либо нарушаем правила владения, переместив значение. ⚠️ Данные не могут быть одновременно в двух структурах.

2. Связка Rc + RefCell для внутренней мутабельности
#

Сам по себе Rc<T> дает только иммутабельный доступ к имеющимся данным. Если вы попробуете изменить значение внутри Rc, компилятор Rust выдаст ошибку.

Что делать, если несколько объектов должны не только читать, но и динамически менять общее состояние? Ответ — обернуть данные в RefCell<T>, получив комбинацию Rc<RefCell<T>>.

Ошибки и паника в рантайме
#

RefCell переносит проверки правил заимствования (borrow checker) с этапа компиляции на этап выполнения программы (runtime). Если вы случайно попытаетесь взять две изменяемые ссылки borrow_mut() одновременно на один и тот же RefCell, программа аварийно завершится с паникой!

Примеры работы с RefCell
Шаг 1/3
 1use std::rc::Rc;
 2use std::cell::RefCell;
 3
 4fn main() {
 5    let users = Rc::new(RefCell::new(vec!["Алиса".to_string()]));
 6
 7    let session1 = Rc::clone(&users);
 8    let session2 = Rc::clone(&users);
 9
10    session1.borrow_mut().push("Макс".to_string());
11    session2.borrow_mut().push("Петр".to_string());
12
13    println!("Онлайн: {:?}", users.borrow());
14}

1. Общий список пользователей в чате

  • Мы моделируем общее состояние users, доступное из разных частей приложения. RefCell позволяет менять данные при множественных владельцах.

3. Проблема циклических ссылок (Rc cycles)
#

Хотя Rc в связке с RefCell дает огромную гибкость, здесь подстерегает опасная коварность: утечка памяти из-за циклических ссылок.

Если структура А содержит Rc узел на структуру Б, а структура Б содержит Rc узел обратно на структуру А, мы получаем замкнутый цикл:

  1. Узел А имеет strong_count == 1 (из-за ссылки от Б).
  2. Узел Б имеет strong_count == 1 (из-за ссылки от А).
  3. Когда внешние переменные a и b выходят из функции, счетчики у обоих объектов уменьшаются лишь до 1, но никогда не становятся равны 0!

В результате деструктор Drop не вызовется ни у одного из узлов, и память в куче останется висеть «навсегда».


4. Решение через Weak (downgrade и upgrade)
#

Чтобы разорвать замкнутый круг циклических ссылок, Rust предоставляет ослабленные ссылки — Weak<T>.

  • Сильная ссылка (Rc<T>) — вы выражаете прямое владение объектом. Объект существует до тех пор, пока strong_count > 0.
  • Слабая ссылка (Weak<T>) — вы выражаете наблюдение за объектом без владения. Создается вызовом Rc::downgrade(&rc).

Weak<T> увеличивает служебный счетчик weak_count, но не влияет на strong_count. Это означает, что главный объект в куче спокойно очистится при обнулении strong_count, даже если на него до сих пор смотрят слабые ссылки!

Как работать со ссылками Weak?
#

Так как объект, на который указывает Weak<T>, мог быть уже очищен в памяти, вы не можете разыменовать Weak напрямую. Для этого используется метод .upgrade():

let weak_ref: Weak<Gadget> = Rc::downgrade(&gadget);

// Пытаемся временно превратить слабую ссылку обратно в сильную:
if let Some(strong_ref) = weak_ref.upgrade() {
    println!("Объект еще жив: {}", strong_ref.id);
} else {
    println!("Объект уже деаллоцирован!");
}

Давайте посмотрим на проблему циклов и их решение через Weak на интерактивных слайдах:

Циклы ссылок и безопасный Weak
Шаг 1/2
 1use std::cell::RefCell;
 2use std::rc::{Rc, Weak};
 3
 4#[derive(Default)]
 5struct Node {
 6    name: String,
 7    next: Option<Rc<RefCell<Node>>>,
 8}
 9
10impl Drop for Node {
11    fn drop(&mut self) {
12        println!("Деструктор узла '{}' вызван!", self.name);
13    }
14}
15
16fn main() {
17    println!("--- Демонстрация сильных ссылок ---");
18    let first = Rc::new(RefCell::new(Node {
19        name: "Узел А".to_string(),
20        next: None,
21    }));
22    
23    let second = Rc::new(RefCell::new(Node {
24        name: "Узел Б".to_string(),
25        next: None,
26    }));
27
28    println!("Сильные ссылки 'first': {}", Rc::strong_count(&first));
29    println!("Сильные ссылки 'second': {}", Rc::strong_count(&second));
30    
31    // Связываем узел А -> узел Б
32    first.borrow_mut().next = Some(Rc::clone(&second));
33    println!("После связывания 'second' strong_count: {}", Rc::strong_count(&second));
34}
35

Подсчет сильных ссылок Rc

  • Rc::strong_count(&a) позволяет узнать количество владельцев объекта в куче.
  • Каждый Rc::clone увеличивает strong_count на 1, а при выходе переменной из scope — уменьшает на 1.
  • Если узел А владеет узлом Б через Rc, счетчик ссылок Б становится равен 2.

Где использовать Rc и Weak
#

  • Деревья и Графы: Узлы хранят сильные ссылки Rc на дочерние элементы, а дочерние элементы хранят слабые ссылки Weak на родительский узел (Parent pointers).
  • Кэширование: Кэш хранит Weak ссылки на объекты. Если объект больше никому в программе не нужен, он автоматически выгружается из памяти.
  • Событийно-ориентированные системы: Подписчики на события хранят Weak ссылки на слушателей.

Проверь свои знания!
#

Пройдите короткий тест, чтобы закрепить знания по работе с Rc, RefCell, цикличным ссылкам и типажу Weak<T>.


Не забывайте, что Rc и Weak предназначены для однопоточного кода. ❗️ Если вам нужно делиться данными между несколькими параллельными потоками — для этого используется потокобезопасная пара Arc<T> и std::sync::Weak<T>, подробный разбор которой вы найдете в отдельной статье про Arc!

История обновлений

  • Добавил подробный разбор циклических ссылок (Rc cycles), проблем утечек памяти и слабого владения через Weak (downgrade/upgrade).
Статья прочитана
Пожалуйста, оцените насколько статья была вам полезна и понятна
Цикл статей
О Rust - Эта статья часть цикла.
Статей прочитано 0/38
0%
📚 Введение и дополнительные материалы
🟢 Начальный уровень (Rust-basic)
Не прочитана
🔵 Средний уровень (Rust-middle)
30 Rc в Rust: Разделённое владение, циклические ссылки и Weak<T> (текущая)
Не прочитана

Связанные статьи