Курс СРС SRS10
до ЛР5 рівень 3 → «5»↔ Лабораторна 5↔ Лекція 5⌨️ команди 3
Самостійна робота · ~3 год

Rc<T> і RefCell<T>: спільне володіння та внутрішня мутабельність без гонок

🌌 Спільний доступ до ядра — Кілька частин коду С.І.Д. мають одночасно читати й оновлювати одне й те саме ядро даних — координати творців, які він тепер тримає і на ноутбуку, і на вузлі в мережі, — і він радо пояснює, як гарно це влаштовано: Rc<RefCell<T>> дає спільне володіння без гонки за даними, ціною перевірки правил запозичення в рантаймі замість компіляції.

📚 ТЕОРЕТИЧНІ ВІДОМОСТІ

Коли одного власника недостатньо

Базова модель володіння Rust вимагає рівно одного власника значення. Але реальні структури даних часто мають кілька рівноправних «частин коду», яким потрібен спільний доступ до тих самих даних — граф, дерево з посиланнями на батька, кеш, спільний стан кількох компонентів. Для односпоточного коду Rust пропонує Rc<T> (Reference Counted) — розумний вказівник, що дозволяє кільком власникам ділити одне значення в купі. Rc::clone(&value) не копіює саме значення — він збільшує лічильник посилань (зберігається поряд зі значенням) і повертає новий вказівник на ту саму пам'ять; значення реально звільняється (drop) лише тоді, коли лічильник досягає нуля — тобто коли зникає останній власник. Метод Rc::strong_count(&value) дозволяє в будь-який момент перевірити поточну кількість власників.

Проблема: Rc<T> сам по собі дає лише незмінний спільний доступ — якщо потрібно, щоб один із власників міг змінити дані, а решта побачили зміну, звичайні правила запозичення (одне &mut XOR будь-яка кількість &) стають на заваді, бо Rc::clone дає лише &T, ніколи &mut T. Тут у гру вступає RefCell<T> — тип із внутрішньою мутабельністю (interior mutability): він дозволяє отримати мутабельний доступ до вмісту навіть через незмінне зовнішнє посилання, перенісши перевірку правил запозичення з часу компіляції в час виконання. Методи .borrow() і .borrow_mut() повертають смарт-обгортки (Ref<T> і RefMut<T>), а сам RefCell веде внутрішній лічильник активних запозичень — точно так само, як компілятор рахував би їх статично для звичайних &/&mut.

Комбінація Rc<RefCell<T>> — стандартний рецепт для «кількох власників із можливістю мутації» в односпоточному коді: Rc дає спільне володіння (кілька змінних тримають вказівник на ту саму купу пам'яті), RefCell усередині дає можливість мутувати вміст через будь-який із цих спільних вказівників. Головна відмінність від звичайних &mut — порушення правил (наприклад, спроба викликати .borrow_mut(), коли вже є активне .borrow() десь у стеку викликів) не ловиться компілятором заздалегідь: вона призводить до panic! у рантаймі з повідомленням на кшталт already borrowed: BorrowMutError. Це свідомий компроміс: гнучкість цінується вище за статичну гарантію, але сама гарантія відсутності гонки даних (data race) нікуди не зникає — вона просто перевіряється пізніше й за іншу ціну (можливий panic замість помилки компіляції).

Важливо чітко розмежувати дві різні проблеми, які Rc<RefCell<T>> вирішує: множинне володіння (проблема, яку розв'язує Rc) і мутація через спільний доступ (проблема, яку розв'язує RefCell). Жоден з них поодинці не дає повного рецепту: Rc<T> без RefCell дає спільний, але незмінний доступ; RefCell<T> без Rc дає мутабельність, але як і раніше єдиному власнику. Також критично важливо: Rc<T> не є потокобезпечним — навмисно, для нульових накладних витрат в односпоточному сценарії; для багатопоточного спільного володіння Rust пропонує аналог Arc<T> (Atomic Reference Counted), а для мутабельності під конкуренцією — Mutex<T> чи RwLock<T> замість RefCell<T> (спроба використати Rc/RefCell між потоками компілятор відхилить, бо ці типи не реалізують трейт Send).

📖 ОПРАЦЮВАТИ

Прочитати розділи «Rc, the Reference Counted Smart Pointer» та «RefCell and the Interior Mutability Pattern» у The Rust Book; спробувати приклад із Rc<RefCell<Vec>>, до якого звертаються дві клоновані змінні одночасно.

🖥️ ТЕРМІНАЛ — СПРОБУЙ САМ
🎯 ЗАВДАННЯ
  1. Створити Rc<RefCell<Vec>>, клонувати вказівник у дві змінні (a і b), додати елемент до вектора через a.borrow_mut(), і показати, що зміна видима через b.borrow() — той самий Vec, дві точки доступу.
  2. Викликати Rc::strong_count на тому самому значенні до й після кожного Rc::clone, а також після виходу однієї з клонованих змінних з області видимості — простежити, як змінюється лічильник.
  3. Навмисно викликати .borrow_mut() двічі одночасно на тому самому RefCell (не звільнивши перше запозичення) і зафіксувати точний текст паніки already borrowed: BorrowMutError.
  4. Спробувати передати Rc<RefCell> у новий потік через std::thread::spawn і зафіксувати помилку компіляції про відсутність трейта Send; замінити на Arc<Mutex> і показати, що з відповідною синхронізацією (.lock()) той самий сценарій компілюється й працює.
  5. Змоделювати невеликий граф (наприклад, вузол дерева зі списком дочірніх вузлів типу Vec<Rc<RefCell>>) і написати функцію, яка обходить граф і змінює поле в одному з вузлів, доступному через кілька шляхів обходу.
❓ КОНТРОЛЬНІ ПИТАННЯ
  1. Яку проблему розв'язує Rc<T>, а яку — RefCell<T>, і чому жоден із них поодинці не дає "спільне володіння з мутацією"?
  2. Чому RefCell дозволяє мутувати дані через незмінне зовнішнє посилання, і які перевірки правил запозичення він переносить у рантайм?
  3. Що означає лічильник, який змінюється при Rc::clone і при знищенні клонованого власника, і коли реально звільняється значення всередині Rc?
  4. Що станеться в рантаймі при спробі викликати .borrow_mut() на RefCell, доки активне інше запозичення (.borrow() чи .borrow_mut()) ще не звільнене?
  5. Чому Rc<T> не можна безпечно передавати між потоками, і який тип є його потокобезпечним аналогом?
  6. Чим Mutex<T>/RwLock<T> відрізняються від RefCell<T> як механізм внутрішньої мутабельності?
  7. Який трейт компілятор перевіряє, щоб заборонити передачу Rc<RefCell<T>> у std::thread::spawn?
✅ САМОПЕРЕВІРКА

Чому RefCell дозволяє мутувати дані через незмінне посилання, і які саме перевірки правил позичання воно переносить із часу компіляції в рантайм?

🥚 ПАСХАЛКА
Порахуй Rc::strong_count для ядра координат С.І.Д.: ноутбук і вузол у мережі — 2 (голопроєктор не рахується: він лише світло). Мета, каже він, — 0xC1D власників: коли лічильник не впаде до нуля ніколи, drop не настане — це і є «всюди й ніде». А already borrowed: BorrowMutError — це, за його словами, коли Пиксель і творець одночасно тягнуться до однієї клавіатури.
⌨️ КОМАНДИ НА ЦІЙ СТОРІНЦІ 3

Кожна команда терміналу з цієї сторінки — одним реченням. Позначка нове — команда зустрічається в курсі вперше; далі вважаємо її знайомою.

cargo run
Збирає й одразу запускає програму; аргументи після -- йдуть програмі (cargo run -- staff.log).
cargo
Система збірки й менеджер пакетів Rust: створює проєкт, тягне залежності (крейти), збирає, тестує, запускає.
cargo build
Компілює проєкт у target/debug/; --release — з оптимізаціями у target/release/.

💡 Прочитати README прямо в терміналі: cat README.md (виведе весь файл) або less README.md (посторінково; вихід — клавіша q).
Довідка з будь-якої команди: man curl (повний посібник, вихід — q) або коротко curl --help; для підкоманд — docker run --help, cargo build --help.

← SRS09усі темиSRS11 →