Salta ai contenuti

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.

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:

5
▶ Provalo tu

Un 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: 1
▶ Provalo tu

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

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 condivisi
Proprietari: 3
▶ Provalo tu

Rc::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:

6
▶ Provalo tu

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

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

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: 6
▶ Provalo tu

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

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: 3
▶ Provalo tu

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

  • 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 usa Arc<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

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