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.
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>, щоб працювати з цим обмеженням незмінності.