Rc<T>, розумний вказівник з лічильником посилань

У більшості випадків володіння є прозорим: ви точно знаєте, яка змінна володіє певним значенням. Проте є випадки, коли одне значення може мати декілька власників. Наприклад, в структурах даних - графах багато ребер можуть вказувати на один вузол, і цей вузол концептуально є володінням усіх ребер, які ведуть у нього. Вузол не повинен бути очищений, поки існують ребра, які вказують на нього, і тому він має власників.

Вам треба явно дозволити множинне володіння, використовуючи тип Rust Rc<T>, що означає лічильник посилань. Тип Rc<T> відстежує кількість посилань на значення, щоб визначити, чи значення все ще використовується. Якщо лишилося нуль посилань на значення, то значення можна очистити, не зробивши жодне посилання некоректним.

Можна уявити собі Rc<T> як телевізор у сімейній кімнаті. Коли одна людина входить, щоб подивитися телевізор, його вмикають. Інші теж можуть зайти до кімнати і подивитися телевізор. Коли остання людина залишає кімнату, телевізор вимикають, бо його більше не використовують. Якщо хтось вимкне телевізор, поки інші все ще його дивитимуться, решта глядачів телевізора обуриться!

Ми використовуємо тип Rc<T>, коли ми хочемо виділити деякі дані в купі, щоб різні частини програми могли їх читати, і не можемо визначити під час компіляції, яка частина останньою завершить використовувати ці дані. Якби ми знали, яка частина завершить останньою, то могли б просто надати цій частині володіння, і були б застосовані звичайні правила володіння часу компіляції.

Зверніть увагу, що Rc<T> використовується тільки в однопотокових сценаріях. Коли ми обговорюватимемо конкурентність у Розділі 16, то розглянемо, як облічувати посилання в багатопотокових програмах.

Використання Rc<T> для спільного використання даних

Повернімося до прикладу зі списком Cons з Блоку коду 15-5. Пам'ятайте, що ми визначили його за допомогою Box<T>. Цього разу створимо два списки, що спільно володіють третім. Концептуально це схоже на Рисунок 15-3:

Два списки, що спільно володіють третім списком

Рисунок 15-3: Два списки, b and c, спільно володіють третім списком, a

Ми створимо список a, що містить 5, а потім 10. Тоді ми зробимо ще два списки: b, що починається з 3 і c, що починається з 4. Обидва списки b та c далі продовжуються першим списком a, що містить 5 і 10. Іншими словами, обидва списки спільно використовують перший, що містить 5 і 10.

Спроба реалізувати цией сценарій за допомогою нашого визначення List із Box<T> не спрацює, як показано в Блоці коду 15-17:

Файл: src/main.rs

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}

Блок коду 15-17: демонстрація, що ми не можемо мати два списки, створені з Box<T>, які намагаються спільно володіти третім списком

Якщо ми скомпілюємо цей код, ми отримаємо цю помилку:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
  --> src/main.rs:11:30
   |
9  |     let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
   |         - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 |     let b = Cons(3, Box::new(a));
   |                              - value moved here
11 |     let c = Cons(4, Box::new(a));
   |                              ^ value used here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` due to previous error

Варіанти Cons володіють даними, що в них знаходяться, тож коли ми створюємо список b, a переміщується в b і b володіє a. Тоді ж, коли ми намагаємося знову використати а при створенні c, нам це заборонено через те, що a було переміщено.

Ми могли б змінити визначення Cons, щоб він зберігав посилання, але тоді нам потрібно було б вказати параметри часу існування. Вказуючи параметри часу існування, ми вказуємо, що кожен елемент у списку буде існувати принаймні стільки ж, скільки весь список. Це так для елементів і списків у Блоці коду 15-17, але не для кожного сценарію.

Натомість ми змінимо наше визначення List, застосувавши Rc<T> замість Box<T>, як показано в Блоці коду 15-18. Варіант Cons тепер буде складатися зі значення і Rc<T>, що вказує на List. Коли ми створюємо b, замість того, перебрати володіння a, ми клонуватимемо Rc<List>, що a містить, збільшуючи таким чином кількість посилань з одного до двох і дозволяючи а та b розділити володіння даними в цьому Rc<List>. Ми також склонуємо a, коли створюватимемо c, збільшуючи кількість посилань з двох до трьох. Кожного разу, як ми викликаємо Rc::clone, кількість посилань на дані в Rc<List> буде збільшуватися, і дані не будуть очищені, поки кількість посилань на них не сягне нуля.

Файл: src/main.rs

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

Блок коду 15-18: визначення List за допомогою Rc<T>

Нам потрібно додати інструкцію use, щоб винести Rc<T> в область видимості, оскільки його немає у прелюдії. У main ми створюємо список, що складається з 5 і 10 і зберігаємо його у новому Rc<List> a. Потім ми створюємо b і c, викликаємо функцію Rc::clone і передаємо посилання на Rc<List> в a як аргумент.

Ми могли б викликати a.clone(), а не Rc::clone(&a), але в Rust діє домовленість використовувати в цьому випадку Rc::clone. Реалізація Rc::clone не робить глибокої копії всіх даних, на відміну від реалізацій реалізації clone для більшості типів. Виклик Rc::clone тільки збільшує кількість посилань, що не забирає багато часу. Глибокі копії даних можуть зайняти багато часу. Використовуючи Rc:::clone для обліку посилань, ми можемо візуально розрізняти clone, що роблять глибоку копію, і ті, які збільшують кількість посилань. При пошуку проблем з продуктивністю в коді нам потрібно лише розглянути clone глибокого копіювання і може не враховувати виклики Rc::clone.

Клонування Rc<T> збільшує кількість посилань

Змінімо наш робочий приклад у Блоці коду 15-18 так, щоб ми могли переглянути зміни лічильника посилань, коли ми створюємо і очищуємо посилання на Rc<List> в a.

У Блоці коду 15-19 ми змінимо main так, щоб вона мала внутрішню область видимості навколо списку c; тоді ми можемо побачити, як змінюється кількість посилань, коли c виходить за межі видимості.

Файл: src/main.rs

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

Блок коду 15-19: виведення лічильника посилань

У кожній точці програми, в якій змінюється кількість посилань, ми виводимо кількість посилань, які ми отримаємо, викликавши функцію Rc::strong_count. Ця функція зветься strong_count, а не просто count, бо тип Rc<T> також має weak_count; ми побачимо, нащо потрібен weak_count, у підрозділі “Запобігання циклам посилань: перетворення Rc<T> на Weak<T> .

Цей код виводить таке:

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished dev [unoptimized + debuginfo] target(s) in 0.45s
     Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

Як ми можемо бачити, Rc<List> в a має початкове значення лічильника 1; потім кожного разу при виклику clone кількість збільшується на 1. Коли c виходить з області видимості, кількість знижується на 1. Там не треба викликати функцію, щоб зменшити лічильник посилань, на відміну від виклику Rc::clone для його збільшення: реалізація трейту Drop зменшує лічильник посилань автоматично, коли Rc<T> виходить з області видимості.

Чого ми не можемо бачити в цьому прикладі, то це того, що коли b, а потім a виходять з області видимості в кінці main, лічильник стає 0, а тоді Rc<List> повністю очищується. Використання Rc<T> дозволяє одному значенню мати кілька власників, а лічильник гарантує, що значення залишається коректним доти, поки існує хоч один із власників.

За допомогою іммутабельних посилань Rc<T> дозволяє спільно використовувати дані лише для читання у багатьох частинах вашої програми. Якби Rc<T> дозволяв також мати кілька повторюваних посилань, ви змогли б порушити одне з правил запозичення, що обговорювалися в Розділі 4: кілька мутабельних позичань до одного місця можуть спричинити гонитву даних і неузгодженість. Але ж мати можливість змінювати дані так зручно! У наступному підрозділі ми обговоримо шаблон внутрішньої мутабельності та тип RefCell<T>, який ви можете використовувати разом з Rc<T> для роботи з цим обмеженням немутабельності.