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>, 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.

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'.

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.