Uno degli obiettivi principali di Rust è prevenire la modifica dei dati immutabili e evitare le condizioni di gara durante la fase di compilazione. In condizioni normali, Rust non consente di modificare i dati tramite un riferimento immutabile. Tuttavia, in sistemi con caching, calcoli pigri o logiche che richiedono la modifica dello stato interno tramite un riferimento, questo può essere necessario. È per questo motivo che è emerso il pattern dell'interior mutability.
Senza l'interior mutability, implementare cache, inizializzazione pigra e molte altre idiomie sarebbe difficile o impossibile, mantenendo la proprietà sicura e i riferimenti. Un esempio classico è una cache all'interno di una funzione che fornisce al mondo esterno solo un riferimento immutabile a se stessa.
In Rust ci sono tipi speciali — Cell e RefCell (e i loro equivalenti per la multi-threading) — che consentono di modificare il valore interno anche attraverso un riferimento immutabile, controllando la sicurezza a tempo di esecuzione, piuttosto che a tempo di compilazione.
Cell<T> — un primitivo di copia, consente di modificare o leggere un valore senza utilizzare riferimenti, ma solo per tipi che implementano Copy.RefCell<T> — consente di ottenere un riferimento mutabile "al volo" anche in presenza di un solo riferimento esterno immutabile, ma se si tenta di ottenere due riferimenti mutabili contemporaneamente o uno mutabile e più immutabili, si verificherà un panico durante l'esecuzione.Esempio di codice:
use std::cell::RefCell; struct Foo { cache: RefCell<Option<u32>>, } impl Foo { fn get_or_compute(&self) -> u32 { if let Some(val) = *self.cache.borrow() { return val; } let computed = 42; *self.cache.borrow_mut() = Some(computed); computed } }
Caratteristiche chiave:
Possiamo usare safely RefCell per strutture multi-thread?
No, RefCell non è thread-safe. Per lavorare in un ambiente multi-thread, usa Mutex o RwLock.
È possibile restituire un riferimento al contenuto di Cell<T>?
No, Cell non restituisce alcun riferimento, copia o aggiorna solo il valore. Funziona solo con tipi Copy, per tutti gli altri usa RefCell.
Cosa succede se chiami borrow_mut due volte di seguito sulla stessa RefCell?
Si verificherà un panico a runtime, poiché RefCell tiene traccia del numero di riferimenti attivi. Il secondo tentativo di ottenere accesso mutabile con un riferimento già esistente porterà a un panico.
In un progetto, per memorizzare qualsiasi stato modificabile all'interno di una struttura, si utilizza sempre RefCell, anche quando si può utilizzare il possesso mutabile. Il codice diventa prolisso, si verificano panici a runtime e diventa difficile effettuare test.
Vantaggi: Può aggirare rapidamente i vincoli del compilatore e ottenere il comportamento desiderato
Svantaggi: Alto rischio di errori a runtime, crash dell'applicazione, logica difficile da debuggare
RefCell viene utilizzato solo per implementare cache pigre in strutture grandi, mentre nel resto del codice si mantiene il possesso e i riferimenti classici. Tutti i valori vengono liberati correttamente, senza panici.
Vantaggi: Logica trasparente, minimizzazione dell'uso dell'interior mutability, codice robusto e prevedibile
Svantaggi: Richiede attenzione ai confini delle modifiche ai dati: la lettura della cache è sempre sicura, la scrittura solo quando necessario e in luoghi ben definiti.