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.
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.
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));
}
Box<T>
che tentano di condividere la proprietà di una terza listaQuando 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.
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)); }
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.
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)); }
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à.