SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI
RefCell<T> e il Modello di Mutabilità Interna
La mutabilità interna è un modello di progettazione in Rust che ti consente di mutare i dati anche quando ci sono riferimenti immutabili a quei dati; normalmente, questa azione è vietata dalle regole di prestito. Per mutare i dati, il modello utilizza codice non sicuro all’interno di una struttura dati per piegare le regole usuali di Rust che governano la mutazione e il prestito. Il codice non sicuro indica al compilatore che stiamo controllando manualmente le regole anziché fare affidamento sul compilatore per controllarle al nostro posto; parleremo di codice non sicuro in modo più approfondito nel Capitolo 19.
Possiamo utilizzare tipi che adottano il modello di mutabilità interna solo quando possiamo garantire che le regole di prestito saranno seguite a runtime, anche se il compilatore non può garantirlo. Il codice non sicuro coinvolto è quindi avvolto in un’API sicura, e il tipo esterno rimane immutabile.
Esploriamo questo concetto guardando il tipo RefCell<T> che segue il modello di mutabilità interna. Eseguire il Controllo delle Regole di Prestito a Runtime con RefCell<T>
A differenza di Rc<T>, il tipo RefCell<T> rappresenta la proprietà singola sui dati che detiene. Quindi, cosa rende diverso RefCell<T> da un tipo come Box<T>? Ricordate le regole di prestito apprese nel Capitolo 4:
- In un dato momento, puoi avere solo (ma non entrambi) un riferimento mutabile o qualsiasi numero di riferimenti immutabili.
- I riferimenti devono essere sempre validi.
Con i riferimenti e Box<T>, le invarianti delle regole di prestito vengono imposte a tempo di compilazione. Con RefCell<T>, queste invarianti vengono imposte a runtime. Con i riferimenti, se si violano queste regole, si ottiene un errore di compilazione. Con RefCell<T>, se si violano queste regole, il programma entrerà in panico ed uscirà.
I vantaggi del controllo delle regole di prestito a tempo di compilazione sono che gli errori verranno rilevati prima nel processo di sviluppo, e non vi è alcun impatto sulle prestazioni a runtime perché tutta l’analisi è completata in anticipo. Per questi motivi, il controllo delle regole di prestito a tempo di compilazione è la scelta migliore nella maggior parte dei casi, ed è per questo che è la predefinita di Rust.
Il vantaggio del controllo delle regole di prestito a runtime, invece, è che vengono consentiti determinati scenari di memoria sicura, dove sarebbero stati vietati dai controlli a tempo di compilazione. L’analisi statica, come il compilatore Rust, è per sua natura conservativa. Alcune proprietà del codice sono impossibili da rilevare analizzando il codice: l’esempio più famoso è il Problema dell’Arresto, che è al di là dello scopo di questo corso ma è un argomento interessante da esplorare.
Poiché alcune analisi sono impossibili, se il compilatore Rust non può essere sicuro che il codice sia conforme alle regole di proprietà, potrebbe rifiutare un programma corretto; in questo modo, è conservativo. Se Rust accettasse un programma incorretto, gli utenti non potrebbero fidarsi delle garanzie che Rust offre. Tuttavia, se Rust rifiuta un programma corretto, il programmatore sarà infastidito, ma non potrà accadere nulla di catastrofico. Il tipo RefCell<T> è utile quando si è sicuri che il proprio codice segua le regole di prestito ma il compilatore non è in grado di comprendere e garantire ciò.
Simile a Rc<T>, RefCell<T> è solo per l’uso in scenari a singolo thread e ti darà un errore di compilazione se provi a usarlo in un contesto multithread. Parleremo di come ottenere la funzionalità di RefCell<T> in un programma multithread nel Capitolo 16.
Ecco un riepilogo dei motivi per scegliere Box<T>, Rc<T>, o RefCell<T>:
- Rc<T> consente proprietari multipli degli stessi dati; Box<T> e RefCell<T> hanno proprietari singoli.
- Box<T> consente prestiti immutabili o mutabili controllati a tempo di compilazione; Rc<T> consente solo prestiti immutabili controllati a tempo di compilazione; RefCell<T> consente prestiti immutabili o mutabili controllati a runtime.
- Poiché RefCell<T> consente prestiti mutabili controllati a runtime, puoi mutare il valore all’interno di RefCell<T> anche quando RefCell<T> è immutabile.
Mutare il valore all’interno di un valore immutabile è il modello di mutabilità interna. Esaminiamo una situazione in cui la mutabilità interna è utile e vediamo come è possibile.
Un Esempio di Utilizzo della Mutabilità Interna: Oggetti Mock
Durante i test, talvolta un programmatore userà un tipo al posto di un altro, al fine di osservare un comportamento specifico e verificarne l’implementazione corretta. Questo tipo sostitutivo è chiamato doppio di test. Pensalo nel senso di un “doppio di stunt” nel mondo del cinema, dove una persona entra e sostituisce un attore per realizzare una particolare scena complicata. I doppi di test sostituiscono altri tipi quando eseguiamo i test. Gli oggetti mock sono tipi specifici di doppi di test che registrano ciò che accade durante un test in modo che tu possa verificare che siano state eseguite le azioni corrette.
Rust non ha oggetti nello stesso senso di altri linguaggi e non dispone di funzionalità di oggetti mock integrate nella libreria standard come fanno alcuni altri linguaggi. Tuttavia, puoi certamente creare una struttura che servirà agli stessi scopi di un oggetto mock.
Ecco lo scenario che testeremo: creeremo una libreria che tiene traccia di un valore rispetto a un valore massimo e invia messaggi in base a quanto il valore attuale si avvicina al valore massimo. Questa libreria potrebbe essere utilizzata per tenere traccia della quota di chiamate API consentite a un utente, ad esempio.
La nostra libreria fornirà solo la funzionalità di tenere traccia di quanto il valore si avvicina al massimo e quali messaggi dovrebbero essere inviati in determinati momenti. Le applicazioni che utilizzano la nostra libreria dovranno fornire il meccanismo per l’invio dei messaggi: l’applicazione potrebbe inserire un messaggio nell’applicazione, inviare una email, inviare un messaggio di testo o altro. La libreria non ha bisogno di sapere quel dettaglio. Tutto ciò di cui ha bisogno è qualcosa che implementi un trait che forniremo chiamato Messenger. Di seguito è mostrato il codice della libreria:
rust
Filename: src/lib.rs
pub trait Messenger {
fn send(&self, msg: &str);
} pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
} impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
} pub fn set_value(&mut self, value: usize) {
self.value = value; let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
Listing 15-20: Una libreria per tenere traccia di quanto un valore si avvicina al massimo e avvisare quando il valore è a determinati livelli
Una parte importante di questo codice è che il trait Messenger ha un metodo chiamato send che prende un riferimento immutabile a self e il testo del messaggio. Questo trait è l’interfaccia di cui ha bisogno il nostro oggetto mock per essere utilizzato nello stesso modo di un oggetto reale. L’altra parte importante è che vogliamo testare il comportamento del metodo set_value su LimitTracker. Possiamo cambiare ciò che passiamo per il parametro value, ma set_value non restituisce nulla su cui fare affermazioni. Vogliamo poter dire che se creiamo un LimitTracker con qualcosa che implementa il trait Messenger e un valore particolare per max, quando passiamo numeri diversi per value, al Messenger viene detto di inviare i messaggi appropriati.
Abbiamo bisogno di un oggetto mock che, anziché inviare una email o un messaggio di testo quando chiamiamo send, tenga traccia solo dei messaggi che gli vengono detti di inviare. Possiamo creare una nuova istanza dell’oggetto mock, creare un LimitTracker che usa l’oggetto mock, chiamare il metodo set_value su LimitTracker e quindi verificare che l’oggetto mock abbia i messaggi che ci aspettiamo. Il Listing 15-21 mostra un tentativo di implementare un oggetto mock per fare proprio questo, ma il bottleneck checker non lo permetterà:
rust
Filename: src/lib.rs
[Questo codice non compila!]
#[cfg(test)]
mod tests {
use super::*; struct MockMessenger {
sent_messages: Vec<String>,
} impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
} impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
} #[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100); limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
Listing 15-21: Un tentativo di implementare un MockMessenger che non è consentito dal bottleneck checker
Questo codice di test definisce una struttura MockMessenger che ha un campo sent_messages con un Vec di valori String per tenere traccia dei messaggi che gli viene detto di inviare. Definiamo anche una funzione associata new per rendere conveniente la creazione di nuovi valori MockMessenger che partono con una lista vuota di messaggi. Implementiamo quindi il trait Messenger per MockMessenger in modo da poter dare un MockMessenger a un LimitTracker. Nella definizione del metodo send, prendiamo il messaggio passato come parametro e lo memorizziamo nella lista sent_messages di MockMessenger.
Nel test, stiamo testando cosa succede quando il LimitTracker viene detto di impostare il valore su qualcosa che è più del 75 percento del valore massimo. Prima, creiamo un nuovo MockMessenger, che partirà con una lista vuota di messaggi. Poi creiamo un nuovo LimitTracker e gli diamo un riferimento al nuovo MockMessenger e un valore massimo di 100. Chiamiamo il metodo set_value su LimitTracker con un valore di 80, che è più del 75 percento di 100. Poi affermiamo che la lista dei messaggi che il MockMessenger sta tenendo traccia dovrebbe ora avere un messaggio al suo interno.
Tuttavia, c’è un problema con questo test, come mostrato qui:
rust
$ cargo test
Compilazione limit-tracker v0.1.0 (file:///projects/limit-tracker)
errore[E0596]: non è possibile prendere in prestito `self.sent_messages` come mutabile, poiché è dietro un riferimento `&`
--> src/lib.rs:58:13
|
2 | fn send(&self, msg: &str);
| ----- aiuto: considera di cambiare quello in un riferimento mutabile: `&mut self`
...
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` è un riferimento `&`, quindi i dati a cui fa riferimento non possono essere presi in prestito come mutabili
Per maggiori informazioni su questo errore, prova `rustc --explain E0596`.
errore: la compilazione `limit-tracker` non è riuscita a causa di un errore precedente
avviso: la compilazione è fallita, in attesa che gli altri lavori finiscano...
Non possiamo modificare il MockMessenger per tenere traccia dei messaggi, perché il metodo send prende un riferimento immutabile a self. Non possiamo neanche accettare la proposta del testo di errore di utilizzare &mut self invece, perché allora la firma di send non corrisponderebbe alla firma nella definizione del trait Messenger (sentiti libero di provare e vedere quale messaggio di errore ottieni).
Questa è una situazione in cui può aiutare la mutabilità interna! Memorizzeremo i sent_messages all’interno di una RefCell<T>, e quindi il metodo send sarà in grado di modificare sent_messages per memorizzare i messaggi che abbiamo visto. Il Listing 15-22 mostra com’è fatto:
rust
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell; struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
} impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
} impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
} #[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
Listing 15-22: Utilizzo di RefCell<T> per mutare un valore interno mentre il valore esterno è considerato immutabile
Il campo sent_messages è ora di tipo RefCell<Vec<String>> invece di Vec<String>. Nella funzione new, creiamo una nuova istanza di RefCell<Vec<String>> intorno al vettore vuoto.
Per l’implementazione del metodo send, il primo parametro è ancora un riferimento immutabile a self, che corrisponde alla definizione del trait. Chiamiamo borrow_mut sulla RefCell<Vec<String>> in self.sent_messages per ottenere un riferimento mutabile al valore all’interno della RefCell<Vec<String>>, che è il vettore. Quindi possiamo chiamare push sul riferimento mutabile al vettore per tenere traccia dei messaggi inviati durante il test.
L’ultimo cambiamento che dobbiamo apportare è nell’asserzione: per vedere quanti elementi ci sono nel vettore interno, chiamiamo borrow sulla RefCell<Vec<String>> per ottenere un riferimento immutabile al vettore.
Ora che hai visto come usare RefCell<T>, approfondiamo come funziona!
Gestione dei Prestiti a Runtime con RefCell<T>
Quando creiamo riferimenti immutabili e mutabili, usiamo rispettivamente la sintassi & e &mut. Con RefCell<T>, utilizziamo i metodi borrow e borrow_mut, che fanno parte dell’API sicura appartenente a RefCell<T>. Il metodo borrow restituisce il tipo di puntatore intelligente Ref<T>, e borrow_mut restituisce il tipo di puntatore intelligente RefMut<T>. Entrambi i tipi implementano Deref, quindi possiamo trattarli come riferimenti regolari.
RefCell<T> tiene traccia di quanti puntatori intelligenti Ref<T> e RefMut<T> sono attualmente attivi. Ogni volta che chiamiamo borrow, RefCell<T> aumenta il suo conteggio di quanti prestiti immutabili sono attivi. Quando un valore Ref<T> esce dallo scope, il conteggio dei prestiti immutabili diminuisce di uno. Proprio come le regole di prestito a tempo di compilazione, RefCell<T> ci consente di avere molti prestiti immutabili o un prestito mutabile in qualsiasi momento.
Se cerchiamo di violare queste regole, invece di ottenere un errore del compilatore come faremmo con i riferimenti, l’implementazione di RefCell<T> causerà un panico a runtime. Il Listing 15-23 mostra una modifica dell’implementazione di send nel Listing 15-22. Stiamo cercando deliberatamente di creare due prestiti mutabili attivi per lo stesso scope per illustrare che RefCell<T> ci impedisce di farlo a runtime.
rust
Filename: src/lib.rs
[This code panics!]
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();
one_borrow.push(String::from(message));
two_borrow.push(String::from(message));
}
}
Listing 15-23: Creazione di due riferimenti mutabili nello stesso scope per vedere che RefCell<T> causerà un panico
Creiamo una variabile one_borrow per il puntatore intelligente RefMut<T> restituito da borrow_mut. Poi creiamo un altro prestito mutabile allo stesso modo nella variabile two_borrow. Questo crea due riferimenti mutabili nello stesso scope, cosa che non è consentita. Quando eseguiamo i test per la nostra libreria, il codice nel Listing 15-23 verrà compilato senza errori, ma il test fallirà:
rust
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
Finished test [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde) running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED failures: ---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures:
tests::it_sends_an_over_75_percent_warning_message
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Nota che il codice ha causato un panico con il messaggio already borrowed: BorrowMutError. Questo è il modo in cui RefCell<T> gestisce le violazioni delle regole di prestito a runtime.
Scegliere di gestire gli errori di prestito a runtime anziché a tempo di compilazione, come abbiamo fatto qui, significa che potresti trovare potenzialmente errori nel tuo codice più avanti nel processo di sviluppo: forse non fino a quando il tuo codice non sarà stato distribuito in produzione. Inoltre, il tuo codice subirebbe una piccola penalità delle prestazioni a runtime a causa del monitoraggio dei prestiti a runtime anziché a tempo di compilazione. Tuttavia, utilizzando RefCell<T>, è possibile scrivere un oggetto mock che può modificarsi per tenere traccia dei messaggi che ha visto mentre lo si utilizza in un contesto in cui sono consentiti solo valori immutabili. Puoi utilizzare RefCell<T> nonostante i suoi compromessi per ottenere più funzionalità rispetto ai riferimenti regolari.
Avere Più Proprietari di Dati Mutabili Unendo Rc<T> e RefCell<T>
Un modo comune per utilizzare RefCell<T> è in combinazione con Rc<T>. Ricorda che Rc<T> ti consente di avere più proprietari di alcuni dati, ma fornisce solo accesso immutabile a quei dati. Se hai un Rc<T> che contiene una RefCell<T>, puoi ottenere un valore che può avere più proprietari e che puoi mutare!
Ad esempio, ricorda l’esempio della lista cons nel Listing 15-18 dove abbiamo usato Rc<T> per consentire a più liste di condividere la proprietà di un’altra lista. Poiché Rc<T> contiene solo valori immutabili, non possiamo modificare nessuno dei valori nella lista una volta creata. Aggiungiamo RefCell<T> per acquisire la possibilità di modificare i valori nelle liste. Il Listing 15-24 mostra che utilizzando una RefCell<T> nella definizione di Cons, possiamo modificare il valore memorizzato in tutte le liste:
rust
Filename: src/main.rs
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
} use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc; fn main() {
let value = Rc::new(RefCell::new(5)); let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a)); *value.borrow_mut() += 10;
println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}
Listing 15-24: Utilizzo di Rc<RefCell<i32>> per creare una Lista che possiamo mutare
Creiamo un valore che è un’istanza di Rc<RefCell<i32>> e lo memorizziamo in una variabile chiamata value in modo da poterlo accedere direttamente in seguito. Quindi creiamo una Lista in a con una variante Cons che tiene value. Dobbiamo clonare value in modo che sia a che value possiedano l’inner 5 value anziché trasferire la proprietà da value a a o avere un prestito da value.
Avvolgiamo la lista a in un Rc<T> così quando creiamo le liste b e c, entrambe possono fare riferimento ad a, che è ciò che abbiamo fatto nel Listing 15-18.
Dopo aver creato le liste in a, b e c, vogliamo aggiungere 10 al valore in value. Facciamo ciò chiamando borrow_mut su value, che utilizza la funzionalità di dereferenziazione automatica di cui abbiamo discusso nel Capitolo 5 (vedi la sezione “Dov’è l’operatore ->?”) per dereferenziare Rc<T> al valore inner RefCell<T>. Il metodo borrow_mut restituisce un puntatore intelligente RefMut<T>, e usiamo l’operatore di dereferenziazione su di esso e cambiamo il valore interno.
Quando stampiamo a, b e c, possiamo vedere che tutti hanno il valore modificato di 15 anziché 5:
rust
$ cargo run
Compilazione cons-list v0.1.0 (file:///projects/cons-list)
Finished dev [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
Questa tecnica è abbastanza interessante! Utilizzando RefCell<T>, abbiamo un valore di Lista esternamente immutabile. Ma possiamo utilizzare i metodi su RefCell<T> che forniscono accesso alla sua mutabilità interna in modo da poter modificare i nostri dati quando necessario. I controlli a runtime delle regole di prestito ci proteggono dalle corse di dati, ed è a volte vale la pena scambiare un po’ di velocità per questa flessibilità nelle nostre strutture dati. Nota che RefCell<T> non funziona per il codice multithread! Mutex<T> è la versione thread-safe di RefCell<T> e ne discuteremo nel Capitolo 16.