Rc<T>, wskaźnik sprytny zliczający referencje
W większości przypadków własność jest jasna: dokładnie wiesz, która zmienna posiada daną wartość. Istnieją jednak sytuacje, gdy pojedyncza wartość może mieć wielu właścicieli. Na przykład, w strukturach danych grafowych, wiele krawędzi może wskazywać na ten sam węzeł, a ten węzeł jest koncepcyjnie własnością wszystkich krawędzi, które na niego wskazują. Węzeł nie powinien być usuwany, chyba że nie ma żadnych krawędzi wskazujących na niego, a zatem nie ma właścicieli.
Musisz jawnie włączyć wielokrotną własność, używając typu Rust Rc<T>, co
jest skrótem od reference counting (zliczanie referencji). Typ Rc<T>
śledzi liczbę referencji do wartości, aby określić, czy wartość jest nadal
używana. Jeśli do wartości istnieje zero referencji, wartość może zostać
usunięta bez unieważniania żadnych referencji.
Wyobraź sobie Rc<T> jako telewizor w pokoju rodzinnym. Kiedy jedna osoba
wchodzi, żeby oglądać telewizor, włącza go. Inni mogą wejść do pokoju i oglądać
telewizor. Kiedy ostatnia osoba opuszcza pokój, wyłącza telewizor, ponieważ
nie jest już używany. Gdyby ktoś wyłączył telewizor, podczas gdy inni nadal
go oglądają, pozostali widzowie wywołaliby zamieszanie!
Typu Rc<T> używamy, gdy chcemy alokować dane na stercie, aby wiele części
naszego programu mogło je odczytywać, a nie możemy w czasie kompilacji
określić, która część zakończy używanie danych jako ostatnia. Gdybyśmy
wiedzieli, która część zakończy używanie jako ostatnia, moglibyśmy po prostu
uczynić tę część właścicielem danych, a normalne zasady własności egzekwowane
w czasie kompilacji weszłyby w życie.
Zauważ, że Rc<T> jest przeznaczony wyłącznie do użytku w scenariuszach
jednowątkowych. Kiedy będziemy omawiać współbieżność w Rozdziale 16,
pokażemy, jak realizować zliczanie referencji w programach
wielowątkowych.
Udostępnianie danych
Wróćmy do naszego przykładu listy konsensusowej z Listingu 15-5. Przypomnijmy,
że zdefiniowaliśmy ją za pomocą Box<T>. Tym razem stworzymy dwie listy, które
obie współdzielą własność trzeciej listy. Koncepcyjnie wygląda to podobnie do
Rysunku 15-3.
Rysunek 15-3: Dwie listy, b i c, współdzielące własność
trzeciej listy, a
Stworzymy listę a, która będzie zawierać 5, a następnie 10. Następnie,
stworzymy dwie kolejne listy: b, która zaczyna się od 3, i c, która
zaczyna się od 4. Obie listy b i c będą kontynuowane do pierwszej listy a
zawierającej 5 i 10. Innymi słowy, obie listy będą współdzielić pierwszą
listę zawierającą 5 i 10.
Próba zaimplementowania tego scenariusza przy użyciu naszej definicji List z
Box<T> nie zadziała, jak pokazano w Listingu 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));
}
Kiedy skompilujemy ten kod, otrzymamy taki błąd:
$ 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
Warianty Cons posiadają przechowywane dane, więc kiedy tworzymy listę b, a
zostaje przeniesione do b, a b staje się właścicielem a. Następnie, gdy
próbujemy ponownie użyć a podczas tworzenia c, nie jest to dozwolone,
ponieważ a zostało przeniesione.
Moglibyśmy zmienić definicję Cons, aby przechowywała referencje, ale wtedy
musielibyśmy określić parametry czasu życia. Określając parametry czasu życia,
określilibyśmy, że każdy element na liście będzie istniał co najmniej tak długo
jak cała lista. Tak jest w przypadku elementów i list w Listingu 15-17, ale
nie w każdym scenariuszu.
Zamiast tego zmienimy naszą definicję List, aby używać Rc<T> zamiast
Box<T>, jak pokazano w Listingu 15-18. Każdy wariant Cons będzie teraz
przechowywał wartość i Rc<T> wskazujący na List. Kiedy tworzymy b,
zamiast przejmować własność a, sklonujemy Rc<List>, które a przechowuje,
powiąkszając tym samym liczbę referencji z jednej do dwóch i pozwalając a i b
współdzielić własność danych w tym Rc<List>. Sklonujemy również a podczas
tworzenia c, zwiększając liczbę referencji z dwóch do trzech. Za każdym
razem, gdy wywołamy Rc::clone, liczba referencji do danych w Rc<List>
wzrośnie, a dane nie zostaną usunięte, chyba że będzie do nich zero referencji.
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));
}
Musimy dodać instrukcję use, aby wprowadzić Rc<T> do zakresu, ponieważ nie
znajduje się on w preambule. W funkcji main tworzymy listę zawierającą 5 i
10 i przechowujemy ją w nowym Rc<List> w zmiennej a. Następnie, gdy
tworzymy b i c, wywołujemy funkcję Rc::clone i przekazujemy referencję
do Rc<List> w a jako argument.
Moglibyśmy wywołać a.clone() zamiast Rc::clone(&a), ale konwencją Rust jest
używanie Rc::clone w tym przypadku. Implementacja Rc::clone nie tworzy
głębokiej kopii wszystkich danych, jak robią to implementacje clone większości
typów. Wywołanie Rc::clone tylko zwiększa licznik referencji, co nie
zabiera wiele czasu. Głębokie kopie danych mogą zajmować dużo czasu. Używając
Rc::clone do zliczania referencji, możemy wizualnie rozróżnić klony
tworzące głębokie kopie od klonów, które zwiększają licznik referencji. Szukając
problemów z wydajnością w kodzie, musimy brać pod uwagę tylko klony tworzące
głębokie kopie i możemy ignorować wywołania Rc::clone.
Klonowanie w celu zwiększenia licznika referencji
Zmieńmy nasz działający przykład z Listingu 15-18, abyśmy mogli zobaczyć,
jak zmieniają się liczniki referencji, gdy tworzymy i usuwamy referencje do
Rc<List> w a.
W Listingu 15-19 zmienimy main tak, aby zawierał wewnętrzny zakres wokół
listy c; wtedy będziemy mogli zobaczyć, jak zmienia się licznik referencji,
gdy c wyjdzie poza zakres.
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));
}
W każdym punkcie programu, w którym zmienia się licznik referencji, wypisujemy
wartość licznika referencji, którą uzyskujemy wywołując funkcję
Rc::strong_count. Funkcja ta nazywa się strong_count (silne zliczanie)
zamiast count (zliczanie), ponieważ typ Rc<T> ma również weak_count
(słabe zliczanie); do czego służy weak_count zobaczymy w sekcji „Zapobieganie
cyklom referencji za pomocą Weak<T>”.
Ten kod wypisuje następujące:
$ 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
Widzimy, że Rc<List> w a ma początkowy licznik referencji równy 1; następnie,
za każdym razem, gdy wywołujemy clone, licznik zwiększa się o 1. Gdy c
wyjdzie poza zakres, licznik zmniejsza się o 1. Nie musimy wywoływać funkcji
w celu zmniejszenia licznika referencji, tak jak musimy wywołać Rc::clone
w celu zwiększenia licznika referencji: Implementacja cechy Drop automatycznie
zmniejsza licznik referencji, gdy wartość Rc<T> wyjdzie poza zakres.
Czego nie widzimy w tym przykładzie, to to, że gdy b, a następnie a wyjdą
poza zakres na końcu main, licznik wynosi 0, a Rc<List> zostaje całkowicie
wyczyszczone. Użycie Rc<T> pozwala jednej wartości mieć wielu właścicieli, a
licznik zapewnia, że wartość pozostaje ważna tak długo, jak długo istnieje
którykolwiek z właścicieli.
Poprzez niemutowalne referencje, Rc<T> pozwala na współdzielenie danych
pomiędzy wieloma częściami programu wyłącznie do odczytu. Gdyby Rc<T>
pozwalało na posiadanie również wielu mutowalnych referencji, mogłoby to
naruszyć jedną z zasad pożyczania omówionych w Rozdziale 4: Wiele mutowalnych
pożyczeń do tego samego miejsca może prowadzić do wyścigów danych i
niekonsekwencji. Ale możliwość modyfikacji danych jest bardzo przydatna! W
następnej sekcji omówimy wzorzec mutowalności wewnętrznej i typ RefCell<T>,
którego można używać w połączeniu z Rc<T> do pracy z tym ograniczeniem
iemutowalności.