Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

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

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

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

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

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

Спільне використання даних

Повернімося до нашого прикладу з cons list у Listing 15-5. Пригадайте, що ми визначили його, використовуючи Box<T>. Цього разу ми створимо два списки, які обидва розділяють власність над третім списком. Концептуально це виглядає подібно до Figure 15-3.

A linked list with the label 'a' pointing to three elements. The first element contains the integer 5 and points to the second element. Th
e second element contains the integer 10 and points to the third element. The third element contains the value 'Nil' that signifies the end of the l
ist; it does not point anywhere. A linked list with the label 'b' points to an element that contains the integer 3 and points to the first element o
f list 'a'. A linked list with the label 'c' points to an element that contains the integer 4 and also points to the first element of list 'a' so th
at the tails of lists 'b' and 'c' are both list 'a'.

Figure 15-3: Two lists, b and c, sharing ownership of a third list, a

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

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

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));
}

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

$ 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
   |
note: if `List` implemented `Clone`, you could clone the value
  --> src/main.rs:1:1
   |
 1 | enum List {
   | ^^^^^^^^^ consider implementing `Clone` for this type
...
10 |     let b = Cons(3, Box::new(a));
   |                              - you could clone this value

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

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

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

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

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));
}

Нам потрібно додати оператор use, щоб ввести Rc<T> в область видимості, тому що він не входить до prelude. У 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 для підрахунку посилань, ми можемо візуально розрізняти клонування типу глибокого копіювання та клонування, що збільшує кількість посилань. Під час пошуку проблем із продуктивністю в коді нам потрібно враховувати лише клони глибокого копіювання і можемо не брати до уваги виклики Rc::clone.

Клонування для збільшення кількості посилань

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

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

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

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

// --snip--

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));
}

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

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

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [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, щоб збільшити кількість посилань: Реалізація трейтa Drop автоматично зменшує кількість посилань, коли значення Rc<T> виходить з області видимості.

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

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