Smart pointer in Rust
Le regole di ownership (possesso) e di prestito (borrowing) coprono quasi tutte le situazioni, ma alcuni casi reali non rientrano bene nello schema «esattamente un proprietario»: un valore la cui dimensione si conosce solo durante l’esecuzione, dati che hanno davvero bisogno di più proprietari, oppure un valore che devi modificare attraverso un riferimento condiviso. Gli smart pointer sono la risposta di Rust a ciascuno di questi casi.
Box<T>: mettere un valore nell’heap
Sezione intitolata “Box<T>: mettere un valore nell’heap”Di norma i valori vivono nello stack, e il compilatore deve conoscerne la dimensione in anticipo. Box<T> sposta un valore nell’heap e ti dà un puntatore di dimensione fissa che punta a lui. È utile quando la dimensione di un tipo non si può conoscere in fase di compilazione, soprattutto con i tipi ricorsivi:
fn main() { let boxed = Box::new(5); println!("{}", *boxed); // * per estrarre il valore}Output:
5Un esempio classico in cui Box è indispensabile: un tipo che contiene sé stesso, come il nodo di una lista concatenata:
enum List { Cons(i32, Box<List>), Nil,}
fn main() { let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
match list { List::Cons(value, _) => println!("Primo valore: {value}"), List::Nil => println!("Vuota"), }}Output:
Primo valore: 1Senza Box, Cons(i32, List) richiederebbe uno spazio infinito: una List che contiene una List che contiene una List, all’infinito. Box<List> invece è un puntatore di dimensione fissa, quindi il compilatore riesce a calcolare quanto spazio serve.
Rc<T>: più proprietari
Sezione intitolata “Rc<T>: più proprietari”L’ownership dice che un valore ha esattamente un proprietario. Quasi sempre è proprio ciò che vuoi, ma a volte hai davvero bisogno che più parti del programma condividano gli stessi dati. Rc<T> («reference counted», con conteggio dei riferimenti) lo permette, tenendo traccia di quanti proprietari esistono in quel momento:
use std::rc::Rc;
fn main() { let a = Rc::new(String::from("dati condivisi")); let b = Rc::clone(&a); // non è una copia profonda: solo un altro proprietario let c = Rc::clone(&a);
println!("{a} {b} {c}"); println!("Proprietari: {}", Rc::strong_count(&a));}Output:
dati condivisi dati condivisi dati condivisiProprietari: 3Rc::clone non copia i dati sottostanti: incrementa solo un contatore e ti restituisce un altro riferimento allo stesso valore. I dati vengono liberati solo quando tutti i proprietari sono usciti dallo scope.
RefCell<T>: modificare attraverso un riferimento condiviso
Sezione intitolata “RefCell<T>: modificare attraverso un riferimento condiviso”Di solito le regole del prestito vengono controllate in fase di compilazione: o tanti riferimenti in sola lettura, o uno solo mutabile, mai entrambi. RefCell<T> sposta questo stesso controllo durante l’esecuzione, e così ti permette di modificare un valore anche attraverso quello che sembra un riferimento condiviso, senza mut:
use std::cell::RefCell;
fn main() { let value = RefCell::new(5);
*value.borrow_mut() += 1;
println!("{}", value.borrow());}Output:
6RefCell si usa spesso insieme a Rc: Rc<RefCell<T>> ti dà un valore con più proprietari che si può comunque modificare, cosa che Rc<T> da solo non permette.
Quando usarli davvero
Sezione intitolata “Quando usarli davvero”Non partire dagli smart pointer. Parti dalla normale ownership e dal prestito, e usa questi strumenti solo quando il compilatore ti sta chiaramente dicendo che non basta: un tipo ricorsivo (Box), un valore che ha davvero bisogno di più proprietari (Rc) oppure una modifica attraverso un riferimento condiviso che le regole del prestito non consentono (RefCell).
Pratica
Sezione intitolata “Pratica”1. Somma una lista concatenata
Sezione intitolata “1. Somma una lista concatenata”Usa l’enum List della sezione su Box. Costruisci la lista 1, 2, 3 e scrivi una funzione fn sum(list: &List) -> i32 che sommi tutti i valori. Suggerimento: fai un match sulla lista. Per Cons(value, rest) il risultato è value più la somma di rest. Per Nil è 0.
Mostra la soluzione
enum List { Cons(i32, Box<List>), Nil,}
fn sum(list: &List) -> i32 { match list { List::Cons(value, rest) => value + sum(rest), List::Nil => 0, }}
fn main() { let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil)))))); println!("Somma: {}", sum(&list));}Output:
Somma: 6La funzione richiama sé stessa sul resto della lista finché non arriva a Nil. Nota che puoi passare rest (un &Box<List>) dove serve un &List: Rust guarda dentro il Box al posto tuo.
2. Una lista della spesa condivisa
Sezione intitolata “2. Una lista della spesa condivisa”Crea una lista della spesa (un Vec<String>) che due persone, alice e bob, possiedono entrambe e a cui possono entrambe aggiungere cose. Ognuno aggiunge un articolo. Poi stampa la lista e il numero di proprietari. Suggerimento: avvolgi il Vec in Rc<RefCell<...>>, dai a ogni persona un Rc::clone e chiama .borrow_mut().push(...) per aggiungere.
Mostra la soluzione
use std::cell::RefCell;use std::rc::Rc;
fn main() { let list = Rc::new(RefCell::new(Vec::new())); let alice = Rc::clone(&list); let bob = Rc::clone(&list);
alice.borrow_mut().push(String::from("latte")); bob.borrow_mut().push(String::from("pane"));
println!("{:?}", list.borrow()); println!("Proprietari: {}", Rc::strong_count(&list));}Output:
["latte", "pane"]Proprietari: 3I proprietari sono tre: list, alice e bob. Tutti e tre puntano allo stesso Vec, quindi un articolo aggiunto tramite alice è visibile anche da bob e da list. Nessuna variabile ha bisogno di mut, perché è RefCell a gestire la modifica.
Riepilogo
Sezione intitolata “Riepilogo”Box<T>salva un valore nell’heap con un puntatore di dimensione fissa: serve per i tipi ricorsivi.Rc<T>permette più proprietari dello stesso valore nel codice a thread singolo, grazie al conteggio dei riferimenti; tra thread usaArc<T>.RefCell<T>controlla le regole del prestito durante l’esecuzione invece che in compilazione, e permette di modificare attraverso un riferimento condiviso; però va in panic se le regole vengono violate.- Usali solo quando la normale ownership e il prestito non bastano per il tuo caso.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Perché un tipo ricorsivo, come il nodo di una lista concatenata, ha bisogno di Box<T>?
2. Che cosa fa davvero Rc::clone(&a)?
3. Quando controlla le regole del prestito RefCell<T>?
4. Che cosa ti dà Rc<RefCell<T>> che Rc<T> da solo non ti dà?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”- Using
Box<T>to Point to Data on the Heap — The Rust Book (in inglese): tutto suBox, compreso l’esempio della lista cons usato in questa pagina. Rc<T>, the Reference Counted Smart Pointer — The Rust Book (in inglese): l’ownership condivisa passo dopo passo, guardando il contatore salire e scendere.RefCell<T>and the Interior Mutability Pattern — The Rust Book (in inglese): quando e perché spostare i controlli del prestito durante l’esecuzione, con un esempio realistico.std::cell— documentazione della libreria standard (in inglese): spiegaCelleRefCell, e quando scegliere l’uno o l’altro.- Rc — Rust by Example (in inglese): un esempio breve ed eseguibile che stampa il conteggio dei riferimenti mentre i proprietari vanno e vengono.