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>, il Puntatore Intelligente con Conteggio dei Reference

Nella maggior parte dei casi, la ownership è chiara: si sa esattamente quale variabile possiede un dato valore. Tuttavia, ci sono casi in cui un singolo valore potrebbe avere più proprietari. Ad esempio, nelle strutture dati grafo1, più archi potrebbero puntare allo stesso nodo, e quel nodo è concettualmente di proprietà di tutti gli archi che puntano ad esso. Un nodo non dovrebbe essere de-allocato a meno che non abbia più archi che puntano ad esso e quindi non abbia proprietari.

È necessario abilitare esplicitamente la ownership multipla utilizzando il type Rc<T>, che è l’abbreviazione di conteggio dei reference (reference counting). Il type Rc<T> tiene traccia del numero di reference a un valore per determinare se il valore è ancora in uso. Se non ci sono reference a un valore, il valore può essere de-allocato senza che alcun reference diventi invalido.

Immaginate Rc<T> come una TV in un soggiorno. Quando una persona entra per guardare la TV, la accende. Altri possono entrare nella stanza e guardare la TV. Quando l’ultima persona esce dalla stanza, spegne la TV perché non la sta più utilizzando. Se qualcuno spegne la TV mentre altri la stanno ancora guardando, ci sarebbe un putiferio da parte degli altri spettatori!

Usiamo il type Rc<T> quando vogliamo allocare alcuni dati nell’heap affinché più parti del nostro programma possano leggerli e non possiamo determinare in fase di compilazione quale parte terminerà l’utilizzo dei dati per ultima. Se sapessimo quale parte terminerà per ultima, potremmo semplicemente impostare quella parte come proprietaria dei dati e applicare le normali regole di ownership che entrerebbero in vigore in fase di compilazione.

Nota che Rc<T> è utilizzabile solo in scenari a thread singolo. Quando parleremo di concorrenza nel Capitolo 16, spiegheremo come eseguire il conteggio dei reference nei programmi multi-thread.

Condividere Dati

Torniamo al nostro esempio di cons list del Listato 15-5. Lo avevamo definito utilizzando Box<T>. Questa volta creeremo due elenchi che condividono la proprietà di un terzo elenco. Concettualmente, questo è simile alla Figura 15-3.

Una lista concatenata con
etichetta 'a' che punta a tre elementi: il primo elemento contiene l’intero 5 e
punta al secondo elemento. Il secondo elemento contiene l’intero 10 e punta al
terzo elemento. Il terzo elemento contiene il valore 'Nil' che indica la fine
della lista; non punta da nessuna parte. Una lista concatenata con etichetta 'b'
punta a un elemento che contiene l’intero 3 e punta al primo elemento della
lista 'a'. Una lista concatenata con etichetta 'c' punta a un elemento che
contiene l’intero 4 e punta anche al primo elemento della lista 'a', in modo che
la coda delle liste 'b' e 'c' sia entrambe la lista 'a'

Figura 15-3: Due liste, b e c, che condividono la proprietà di una terza lista, a

Creeremo la lista a che contiene 5 e poi 10. Quindi creeremo altre due liste: b che inizia con 3 e c che inizia con 4. Entrambe le liste b e c proseguiranno quindi con la prima lista a contenente 5 e 10. In altre parole, entrambe le liste condivideranno la prima lista contenente 5 e 10.

Cercare di implementare questo scenario utilizzando la nostra definizione di Lista con Box<T> non funzionerebbe, come mostrato nel Listato 15-17.

File: src/main.rs
enum Lista {
    Cons(i32, Box<Lista>),
    Nil,
}

use crate::Lista::{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));
}
Listato 15-17: Dimostrazione che non è consentito avere due liste che utilizzano Box<T> che tentano di condividere la proprietà di una terza lista

Quando compiliamo questo codice, otteniamo questo errore:

$ cargo run
   Compiling cons-list v0.1.0 (file:///progetti/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 `Lista`, 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` (bin "cons-list") due to 1 previous error

Le varianti Cons hanno ownership dei dati che contengono, quindi quando creiamo la lista b, a viene spostata in b e b possiede a. Quindi, quando proviamo a usare di nuovo a durante la creazione di c, non ci viene consentito perché a è stata spostata.

Potremmo modificare la definizione di Cons per contenere dei reference, ma in tal caso dovremmo poi specificare anche i parametri di lifetime. Specificando i parametri di lifetime, specificheremmo che ogni elemento della lista avrà longevità di almeno quanto l’intera lista. Questo vale per gli elementi e le liste nel Listato 15-17, ma non in tutti gli scenari.

Invece, modificheremo la nostra definizione di Lista per usare Rc<T> al posto di Box<T>, come mostrato nel Listato 15-18. Ogni variante di Cons ora conterrà un valore e un Rc<T> che punta a una Lista. Quando creiamo b, invece di assumere la proprietà di a, cloneremo Rc<Lista> che contiene a, aumentando così il numero di reference da 1 a 2 e lasciando che a e b condividano la ownership dei dati in quella Rc<Lista>. Cloneremo anche a quando creiamo c, aumentando il numero di reference da 2 a 3. Ogni volta che chiamiamo Rc::clone, il conteggio dei reference ai dati all’interno di Rc<Lista> aumenterà e i dati non verranno de-allocati a meno che non ci siano zero riferimenti ad essi.

File: src/main.rs
enum Lista {
    Cons(i32, Rc<Lista>),
    Nil,
}

use crate::Lista::{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));
}
Listato 15-18: Una definizione di Lista che usa Rc<T>

Dobbiamo aggiungere un’istruzione use per portare Rc<T> nello scope perché non è nel preludio. In main, creiamo la lista contenente 5 e 10 e la memorizziamo in una nuova Rc<Lista> in a. Quindi, quando creiamo b e c, chiamiamo la funzione Rc::clone e passiamo un reference a Rc<Lista> in a come argomento.

Avremmo potuto chiamare a.clone() invece di Rc::clone(&a), ma la convenzione di Rust in questo caso prevede di usare Rc::clone. L’implementazione di Rc::clone non esegue una copia profonda (deep copy) di tutti i dati come fanno la maggior parte delle implementazioni di clone dei vari type. La chiamata a Rc::clone incrementa solo il conteggio dei reference, il che non richiede molto tempo. Le copie profonde dei dati possono richiedere molto tempo. Utilizzando Rc::clone per il conteggio dei reference, possiamo distinguere visivamente tra i type che clonano usando la copia profonda e quelli che aumentano il conteggio dei reference. Quando cercheremo problemi di prestazioni nel codice, possiamo considerare solo i type che clonano tramite copia profonda e possiamo ignorare le chiamate a Rc::clone.

Clonare per Aumentare il Conteggio dei Reference

Modifichiamo il nostro esempio di lavoro nel Listato 15-18 in modo da poter vedere i conteggi dei reference cambiare quando creiamo ed eliminiamo reference a Rc<Lista> in a.

Nel Listato 15-19, modificheremo main in modo che abbia uno scope interno attorno alla lista c; poi potremo vedere come cambia il conteggio dei reference quando c esce dallo scope.

File: src/main.rs
enum Lista {
    Cons(i32, Rc<Lista>),
    Nil,
}

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

// --taglio--

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("conteggio dopo la creazione di a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("conteggio dopo la creazione di b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("conteggio dopo la creazione di c = {}", Rc::strong_count(&a));
    }
    println!("conteggio dopo che c è uscita dallo scope c = {}", Rc::strong_count(&a));
}
Listato 15-19: Stampa del conteggio dei reference

In ogni punto del programma in cui il conteggio dei reference cambia, stampiamo il conteggio dei reference, che otteniamo chiamando la funzione Rc::strong_count. Questa funzione si chiama strong_count anziché count perché il type Rc<T> ha anche un weak_count; vedremo a cosa serve weak_count in “Prevenire Cicli di Riferimento Usando Weak<T>.

Questo codice stampa quanto segue:

$ cargo run
   Compiling cons-list v0.1.0 (file:///progetti/cons-list)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.17s
     Running `target/debug/cons-list`
conteggio dopo la creazione di a = 1
conteggio dopo la creazione di b = 2
conteggio dopo la creazione di c = 3
conteggio dopo che c è uscita dallo scope c = 2

Possiamo vedere che Rc<Lista> in a ha un conteggio dei reference iniziale pari a 1; quindi ogni volta che chiamiamo clone, il conteggio aumenta di 1. Quando c esce dallo scope, il conteggio diminuisce di 1. Non dobbiamo chiamare una funzione per diminuire il conteggio dei reference, come dobbiamo chiamare Rc::clone per aumentarlo: l’implementazione del trait Drop diminuisce il conteggio dei reference automaticamente quando un valore Rc<T> esce dallo scope.

Ciò che non possiamo vedere in questo esempio è che quando b e poi a escono dallo scope alla fine di main, il conteggio diventa 0 e Rc<Lista> viene de-allocato completamente. L’utilizzo di Rc<T> consente a un singolo valore di avere più proprietari e il conteggio garantisce che il valore rimanga valido finché uno qualsiasi dei proprietari esiste ancora.

Tramite reference immutabili, Rc<T> consente di condividere dati tra più parti del programma in sola lettura. Se Rc<T> consentisse di avere anche più reference mutabili, si potrebbe violare una delle regole di prestito discusse nel Capitolo 4: più prestiti mutabili nello stesso posto possono causare conflitti di dati e incongruenze. Ma poter mutare i dati è molto utile! Nella prossima sezione, discuteremo di mutabilità interna e del type RefCell<T> che è possibile utilizzare insieme a Rc<T> per lavorare con questa restrizione di immutabilità.


  1. Grafo su wikipedia