RefCell<T>
e il Modello di Mutabilità Interna
Interior mutability è un modello di design in Rust che consente di mutare i
dati anche in presenza di reference immutabili a tali dati; normalmente,
questa azione non è consentita dalle regole di prestito. Per mutare i dati, il
modello utilizza codice unsafe
all’interno di una struttura dati per
modificare le normali regole di Rust che governano la mutabilità e il prestito.
Il codice unsafe
indica al compilatore che stiamo controllando le regole
manualmente invece di affidarci al compilatore affinché le controlli per noi;
approfondiremo il codice unsafe
nel Capitolo 20.
Possiamo utilizzare type che utilizzano il modello di mutabilità interna solo
quando possiamo garantire che le regole di prestito vengano rispettate durante
l’esecuzione, anche se il compilatore non può garantirlo. Il codice unsafe
coinvolto viene quindi racchiuso in un’API sicura e il type esterno rimane
immutabile.
Esploriamo questo concetto esaminando il type RefCell<T>
che segue il
modello di mutabilità interna.
Applicare le Regole di Prestito in Fase di Esecuzione
A differenza di Rc<T>
, il type RefCell<T>
rappresenta la ownership
singola sui dati che contiene. Quindi, cosa rende RefCell<T>
diverso da un
type come Box<T>
? Ricorda le regole di prestito apprese nel Capitolo 4:
- In qualsiasi momento, puoi avere o un reference mutabile o un numero qualsiasi di reference immutabili (ma non entrambi).
- I reference devono essere sempre validi.
Con i reference e Box<T>
, le invarianti delle regole di prestito vengono
applicate in fase di compilazione. Con RefCell<T>
, queste invarianti vengono
applicate in fase di esecuzione. Con i reference, se si violano queste regole,
si otterrà un errore di compilazione. Con RefCell<T>
, se si violano queste
regole, il programma andrà in panic e si chiuderà.
I vantaggi del controllo delle regole di prestito in fase di compilazione sono che gli errori vengono rilevati durante il processo di sviluppo e non vi è alcun impatto sulle prestazioni in fase di esecuzione perché tutta l’analisi viene completata in anticipo. Per questi motivi, il controllo delle regole di prestito in fase di compilazione è la scelta migliore nella maggior parte dei casi, ed è per questo che questa è la scelta predefinita di Rust.
Il vantaggio del controllo delle regole di prestito in fase di esecuzione è che vengono consentiti determinati scenari di sicurezza della memoria, laddove sarebbero stati non consentiti dai controlli in fase di compilazione. L’analisi statica, come quella effettuata dal compilatore Rust, è intrinsecamente conservativa. Alcune proprietà del codice sono impossibili da rilevare analizzando il codice: l’esempio più famoso è il problema della terminazione (Halting Problem), che esula dall’ambito di questo libro ma è un argomento interessante da approfondire se vuoi.
Poiché alcune analisi sono impossibili, se il compilatore Rust non può essere
sicuro che il codice sia conforme alle regole di ownership, potrebbe rifiutare
di compilare un programma corretto; in questo modo, è conservativo. Se Rust
accettasse un programma errato, gli utenti non potrebbero fidarsi delle garanzie
fornite da Rust. Tuttavia, se Rust rifiuta di compilare un programma corretto,
il programmatore non sarà certo contento, anche se non è nulla di catastrofico.
Il type RefCell<T>
è utile quando si è certi che il codice segua le regole
di prestito, ma il compilatore non è in grado di comprenderlo e garantirlo.
Simile a Rc<T>
, RefCell<T>
è utilizzabile solo in scenari a thread singolo
e genererà un errore in fase di compilazione se si tenta di utilizzarlo in un
contesto multi-thread. Parleremo di come ottenere la funzionalità di
RefCell<T>
in un programma multi-thread nel Capitolo 16.
Ecco un riepilogo delle ragioni per scegliere Box<T>
, Rc<T>
o RefCell<T>
:
Rc<T>
consente più proprietari degli stessi dati;Box<T>
eRefCell<T>
hanno proprietari singoli.Box<T>
consente prestiti immutabili o mutabili controllati in fase di compilazione;Rc<T>
consente solo prestiti immutabili controllati in fase di compilazione;RefCell<T>
consente prestiti immutabili o mutabili controllati in fase di esecuzione.- Poiché
RefCell<T>
consente prestiti mutabili controllati in fase di esecuzione, è possibile modificare il valore all’interno diRefCell<T>
anche quandoRefCell<T>
è immutabile.
Mutare il valore all’interno di un valore immutabile è il modello di Interior Mutability . Esaminiamo una situazione in cui la mutabilità interna è utile e vediamo come sia possibile.
Usare la Mutabilità Interna
Una conseguenza delle regole di prestito è che quando si ha un valore immutabile, non è possibile prenderlo in prestito mutabilmente. Ad esempio, questo codice non verrà compilato:
fn main() {
let x = 5;
let y = &mut x;
}
Se provassi a compilare questo codice, otterresti il seguente errore:
$ cargo run
Compiling borrowing v0.1.0 (file:///progetti/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
--> src/main.rs:3:13
|
3 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` (bin "borrowing") due to 1 previous error
Tuttavia, ci sono situazioni in cui sarebbe utile che un valore mutasse se
stesso nei suoi metodi, ma apparisse immutabile ad altro codice. Il codice
esterno ai metodi del valore non sarebbe in grado di mutare il valore. Usare
RefCell<T>
è un modo per ottenere la possibilità di avere una mutabilità
interna, senza però aggirare completamente le regole di prestito: il controllore
di prestito nel compilatore consente questa mutabilità interna e le regole di
prestito vengono invece verificate durante l’esecuzione. Se si violano le
regole, si otterrà un panic!
invece di un errore del compilatore.
Esaminiamo un esempio pratico in cui possiamo usare RefCell<T>
per mutare un
valore immutabile e vediamo perché è utile.
Testare con gli Oggetti Mock
A volte, durante i test, un programmatore usa un type al posto di un altro, per osservare un comportamento particolare e verificare che sia implementato correttamente. Questo type segnaposto è chiamato test double (doppione di test). Pensalo come ad una controfigura nel cinema, dove una persona interviene e sostituisce un attore per girare una scena particolarmente difficile. I test double sostituiscono altri type durante l’esecuzione dei test. Gli oggetti mock sono type specifici di test double che registrano ciò che accade durante un test, in modo da poter verificare che sono state eseguite le azioni corrette.
Rust non ha oggetti nello stesso senso in cui li hanno altri linguaggi, e Rust non ha funzionalità di oggetti mock integrate nella libreria standard come altri linguaggi. Tuttavia, è sicuramente possibile creare una struct che svolgerà le stesse funzioni 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 corrente è vicino al valore massimo. Questa libreria potrebbe essere utilizzata, ad esempio, per tenere traccia della quota di un utente per il numero di chiamate API che gli è consentito effettuare.
La nostra libreria fornirà solo la funzionalità di tracciare quanto un valore è
vicino al massimo e quali messaggi dovrebbero essere inviati e in quali momenti.
Le applicazioni che utilizzano la nostra libreria dovranno fornire il meccanismo
per l’invio dei messaggi: l’applicazione potrebbe mostrare il messaggio
direttamente all’utente, inviare un’email, inviare un messaggio di testo o fare
altro. La libreria non ha bisogno di conoscere questo dettaglio. Tutto ciò di
cui ha bisogno è qualcosa che implementi un trait che forniremo chiamato
Messaggero
. Il Listato 15-20 mostra il codice della libreria.
pub trait Messaggero {
fn invia(&self, msg: &str);
}
pub struct TracciaLimiti<'a, T: Messaggero> {
messaggero: &'a T,
valore: usize,
max: usize,
}
impl<'a, T> TracciaLimiti<'a, T>
where
T: Messaggero,
{
pub fn new(messaggero: &'a T, max: usize) -> TracciaLimiti<'a, T> {
TracciaLimiti {
messaggero,
valore: 0,
max,
}
}
pub fn setta_valore(&mut self, valore: usize) {
self.valore = valore;
let percentuale_di_max = self.valore as f64 / self.max as f64;
if percentuale_di_max >= 1.0 {
self.messaggero
.invia("Errore: Hai superato la tua quota!");
} else if percentuale_di_max >= 0.9 {
self.messaggero
.invia("Avviso urgente: Hai utilizzato oltre il 90% della tua quota!");
} else if percentuale_di_max >= 0.75 {
self.messaggero
.invia("Avviso: Hai utilizzato oltre il 75% della tua quota!");
}
}
}
Una parte importante di questo codice è che il trait Messaggero
ha un metodo
chiamato invia
che accetta un reference immutabile a self
e il testo del
messaggio. Questo trait è l’interfaccia che il nostro oggetto mock deve
implementare in modo che il mock possa essere utilizzato allo stesso modo di
un oggetto reale. L’altra parte importante è che vogliamo testare il
comportamento del metodo setta_valore
su TracciaLimiti
. Possiamo modificare
ciò che passiamo per il parametro valore
, ma setta_valore
non restituisce
nulla su cui fare asserzioni. Vogliamo poter dire che se creiamo un
TracciaLimiti
con qualcosa che implementa il trait Messaggero
e un valore
specifico per max
, al messaggero viene detto di inviare i messaggi appropriati
quando passiamo numeri diversi per valore
.
Abbiamo bisogno di un oggetto mock che, invece di inviare un’email o un
messaggio di testo quando chiamiamo invia
, tenga traccia solo dei messaggi che
gli viene detto di inviare. Possiamo creare una nuova istanza dell’oggetto
mock, creare un TracciaLimiti
che utilizzi l’oggetto mock, chiamare il
metodo setta_valore
su TracciaLimiti
e quindi verificare che l’oggetto
mock contenga i messaggi che ci aspettiamo. Il Listato 15-21 mostra un
tentativo di implementare un oggetto mock per fare proprio questo, ma il
controllo dei prestiti non lo consente.
pub trait Messaggero {
fn invia(&self, msg: &str);
}
pub struct TracciaLimiti<'a, T: Messaggero> {
messaggero: &'a T,
valore: usize,
max: usize,
}
impl<'a, T> TracciaLimiti<'a, T>
where
T: Messaggero,
{
pub fn new(messaggero: &'a T, max: usize) -> TracciaLimiti<'a, T> {
TracciaLimiti {
messaggero,
valore: 0,
max,
}
}
pub fn setta_valore(&mut self, valore: usize) {
self.valore = valore;
let percentuale_di_max = self.valore as f64 / self.max as f64;
if percentuale_di_max >= 1.0 {
self.messaggero.invia("Errore: Hai superato la tua quota!");
} else if percentuale_di_max >= 0.9 {
self.messaggero
.invia("Avviso urgente: Hai utilizzato oltre il 90% della tua quota!");
} else if percentuale_di_max >= 0.75 {
self.messaggero
.invia("Avviso: Hai utilizzato oltre il 75% della tua quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockMessaggero {
messaggi_inviati: Vec<String>,
}
impl MockMessaggero {
fn new() -> MockMessaggero {
MockMessaggero {
messaggi_inviati: vec![],
}
}
}
impl Messaggero for MockMessaggero {
fn invia(&self, messaggio: &str) {
self.messaggi_inviati.push(String::from(messaggio));
}
}
#[test]
fn invia_un_messaggio_di_avviso_di_superamento_del_75_percento() {
let mock_messaggero = MockMessaggero::new();
let mut traccia_limiti = TracciaLimiti::new(&mock_messaggero, 100);
traccia_limiti.setta_valore(80);
assert_eq!(mock_messaggero.messaggi_inviati.len(), 1);
}
}
MockMessaggero
non consentito dal controllo dei prestitiQuesto codice di test definisce una struct MockMessaggero
che ha un campo
messaggi_inviati
con un Vec
di valori String
per tenere traccia dei
messaggi che gli viene chiesto di inviare. Definiamo anche una funzione
associata new
per semplificare la creazione di nuovi valori MockMessaggero
che iniziano con un elenco vuoto di messaggi. Implementiamo quindi il trait
Messaggero
per MockMessaggero
in modo da poter assegnare un MockMessaggero
a un TracciaLimiti
. Nella definizione del metodo invia
, prendiamo il
messaggio passato come parametro e lo memorizziamo nella lista MockMessaggero
di messaggi_inviati
.
Nel test, stiamo testando cosa succede quando a TracciaLimiti
viene chiesto di
impostare valore
a un valore superiore al 75% del valore max
. Per prima cosa
creiamo un nuovo MockMessaggero
, che inizierà con una lista vuota di messaggi.
Quindi creiamo un nuovo TracciaLimiti
e gli diamo un reference al nuovo
MockMessaggero
e un valore max
di 100
. Chiamiamo il metodo setta_valore
su TracciaLimiti
con un valore di 80
, che è superiore al 75% di 100. Quindi
verifichiamo che la lista di messaggi di cui MockMessaggero
sta tenendo
traccia dovrebbe ora contenere un messaggio.
Tuttavia, c’è un problema con questo test, come mostrato qui:
$ cargo test
Compiling traccia-limiti v0.1.0 (file:///progetti/traccia-limiti)
error[E0596]: cannot borrow `self.messaggi_inviati` as mutable, as it is behind a `&` reference
--> src/lib.rs:59:13
|
59 | self.messaggi_inviati.push(String::from(messaggio));
| ^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
|
2 ~ fn invia(&mut self, msg: &str);
3 | }
...
57 | impl Messaggero for MockMessaggero {
58 ~ fn invia(&mut self, messaggio: &str) {
|
For more information about this error, try `rustc --explain E0596`.
error: could not compile `traccia-limiti` (lib test) due to 1 previous error
Non possiamo modificare MockMessaggero
per tenere traccia dei messaggi perché
il metodo invia
accetta un reference immutabile a self
. Inoltre, non
possiamo accettare il suggerimento dal testo di errore di utilizzare &mut self
sia nel metodo impl
che nella definizione del trait. Non vogliamo modificare
il trait Messaggero
solo per il funzionamento del test. Dobbiamo invece
trovare un modo per far funzionare il nostro codice di test correttamente con il
nostro design esistente.
Questa è una situazione in cui la mutabilità interna può essere d’aiuto!
Memorizzeremo messaggi_inviati
all’interno di un RefCell<T>
, e poi il metodo
invia
sarà in grado di modificare messaggi_inviati
per memorizzare i
messaggi che abbiamo visto. Il Listato 15-22 mostra come fare.
pub trait Messaggero {
fn invia(&self, msg: &str);
}
pub struct TracciaLimiti<'a, T: Messaggero> {
messaggero: &'a T,
valore: usize,
max: usize,
}
impl<'a, T> TracciaLimiti<'a, T>
where
T: Messaggero,
{
pub fn new(messaggero: &'a T, max: usize) -> TracciaLimiti<'a, T> {
TracciaLimiti {
messaggero,
valore: 0,
max,
}
}
pub fn set_valore(&mut self, valore: usize) {
self.valore = valore;
let percentuale_di_max = self.valore as f64 / self.max as f64;
if percentuale_di_max >= 1.0 {
self.messaggero.invia("Errore: Hai superato la tua quota!");
} else if percentuale_di_max >= 0.9 {
self.messaggero
.invia("Avviso urgente: Hai utilizzato oltre il 90% della tua quota!");
} else if percentuale_di_max >= 0.75 {
self.messaggero
.invia("Avviso: Hai utilizzato oltre il 75% della tua quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessaggero {
messaggi_inviati: RefCell<Vec<String>>,
}
impl MockMessaggero {
fn new() -> MockMessaggero {
MockMessaggero {
messaggi_inviati: RefCell::new(vec![]),
}
}
}
impl Messaggero for MockMessaggero {
fn invia(&self, messaggio: &str) {
self.messaggi_inviati.borrow_mut().push(String::from(messaggio));
}
}
#[test]
fn invia_un_messaggio_di_avviso_di_superamento_del_75_percento() {
// --taglio--
let mock_messaggero = MockMessaggero::new();
let mut traccia_limiti = TracciaLimiti::new(&mock_messaggero, 100);
traccia_limiti.set_valore(80);
assert_eq!(mock_messaggero.messaggi_inviati.borrow().len(), 1);
}
}
RefCell<T>
per modificare un valore interno mentre il valore esterno è considerato immutabileIl campo messaggi_inviati
è ora di type RefCell<Vec<String>>
invece di
Vec<String>
. Nella funzione new
, creiamo una nuova istanza di
RefCell<Vec<String>>
incapsulando il vettore vuoto.
Per l’implementazione del metodo send
, il primo parametro è ancora un prestito
immutabile di self
, che corrisponde alla definizione del trait. Chiamiamo
borrow_mut
su RefCell<Vec<String>>
in self.messaggi_inviati
per ottenere
un reference mutabile al valore all’interno di RefCell<Vec<String>>
, che è
il vettore. Quindi possiamo chiamare push
sul reference mutabile al vettore
per tenere traccia dei messaggi inviati durante il test.
L’ultima modifica che dobbiamo apportare riguarda l’asserzione: per vedere
quanti elementi ci sono nel vettore interno, chiamiamo borrow
su
RefCell<Vec<String>>
per ottenere un reference immutabile al vettore.
Ora che hai visto come usare RefCell<T>
, approfondiamo il suo funzionamento!
Tracciare i Prestiti in Fase di Esecuzione
Quando creiamo reference immutabili e mutabili, utilizziamo rispettivamente la
sintassi &
e &mut
. Con RefCell<T>
, utilizziamo i metodi borrow
e
borrow_mut
, che fanno parte dell’API sicura di RefCell<T>
. Il metodo
borrow
restituisce il type di puntatore intelligente Ref<T>
, mentre
borrow_mut
restituisce il type di puntatore intelligente RefMut<T>
.
Entrambi i type implementano Deref
, quindi possiamo trattarli come normali
reference.
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 conteggio dei prestiti immutabili attivi. Quando un
valore Ref<T>
esce dallo scope, il conteggio dei prestiti immutabili
diminuisce di 1. Proprio come per le regole di prestito in fase di compilazione,
RefCell<T>
ci consente di avere molti prestiti immutabili o un prestito
mutabile in qualsiasi momento.
Se proviamo a violare queste regole, anziché ottenere un errore di compilazione
come accadrebbe con i reference, l’implementazione di RefCell<T>
andrà in
panic in fase di esecuzione. Il Listato 15-23 mostra una modifica
dell’implementazione di invia
nel Listato 15-22. Stiamo deliberatamente
cercando di creare due prestiti mutabili attivi nello stesso scope per
dimostrare che RefCell<T>
ci impedisce di farlo in fase di esecuzione.
pub trait Messaggero {
fn invia(&self, msg: &str);
}
pub struct TracciaLimiti<'a, T: Messaggero> {
messaggero: &'a T,
valore: usize,
max: usize,
}
impl<'a, T> TracciaLimiti<'a, T>
where
T: Messaggero,
{
pub fn new(messaggero: &'a T, max: usize) -> TracciaLimiti<'a, T> {
TracciaLimiti {
messaggero,
valore: 0,
max,
}
}
pub fn set_valore(&mut self, valore: usize) {
self.valore = valore;
let percentuale_di_max = self.valore as f64 / self.max as f64;
if percentuale_di_max >= 1.0 {
self.messaggero.invia("Errore: Hai superato la tua quota!");
} else if percentuale_di_max >= 0.9 {
self.messaggero
.invia("Avviso urgente: Hai utilizzato oltre il 90% della tua quota!");
} else if percentuale_di_max >= 0.75 {
self.messaggero
.invia("Avviso: Hai utilizzato oltre il 75% della tua quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessaggero {
messaggi_inviati: RefCell<Vec<String>>,
}
impl MockMessaggero {
fn new() -> MockMessaggero {
MockMessaggero {
messaggi_inviati: RefCell::new(vec![]),
}
}
}
impl Messaggero for MockMessaggero {
fn invia(&self, messaggio: &str) {
let mut borrow_uno = self.messaggi_inviati.borrow_mut();
let mut borrow_due = self.messaggi_inviati.borrow_mut();
borrow_uno.push(String::from(messaggio));
borrow_due.push(String::from(messaggio));
}
}
#[test]
fn invia_un_messaggio_di_avviso_di_superamento_del_75_percento() {
let mock_messaggero = MockMessaggero::new();
let mut traccia_limiti = TracciaLimiti::new(&mock_messaggero, 100);
traccia_limiti.set_valore(80);
assert_eq!(mock_messaggero.messaggi_inviati.borrow().len(), 1);
}
}
RefCell<T>
generi un panicCreiamo una variabile borrow_uno
per il puntatore intelligente RefMut<T>
restituito da borrow_mut
. Quindi creiamo un altro prestito mutabile allo
stesso modo nella variabile borrow_due
. Questo crea due reference mutabili
nello stesso scope, cosa non consentita. Quando eseguiamo i test per la nostra
libreria, il codice nel Listato 15-23 verrà compilato senza errori, ma il test
fallirà:
$ cargo test
Compiling traccia-limiti v0.1.0 (file:///progetti/traccia-limiti)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.01s
Running unittests src/lib.rs (target/debug/deps/traccia_limiti-b118b9738b4f0e54)
running 1 test
test tests::invia_un_messaggio_di_avviso_di_superamento_del_75_percento ... FAILED
failures:
---- tests::invia_un_messaggio_di_avviso_di_superamento_del_75_percento stdout ----
thread 'tests::invia_un_messaggio_di_avviso_di_superamento_del_75_percento' panicked at src/lib.rs:61:56:
already borrowed: BorrowMutError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::invia_un_messaggio_di_avviso_di_superamento_del_75_percento
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
Nota che il codice è andato in panic con il messaggio already borrowed: BorrowMutError
. Ecco come RefCell<T>
gestisce le violazioni delle regole di
prestito in fase di esecuzione.
Scegliere di rilevare gli errori di prestito durante l’esecuzione anziché in
fase di compilazione, come abbiamo fatto qui, significa che potenzialmente si
troverebbero errori nel codice in una fase successiva del processo di sviluppo:
probabilmente non prima del rilascio del codice in produzione. Inoltre, il
codice subirebbe una piccola penalizzazione delle prestazioni durante
l’esecuzione a causa del monitoraggio dei prestiti durante l’esecuzione anziché
in fase di compilazione. Tuttavia, l’utilizzo di RefCell<T>
consente di
scrivere un oggetto fittizio in grado di modificarsi per tenere traccia dei
messaggi visualizzati durante l’utilizzo in un contesto in cui sono consentiti
solo valori immutabili. È possibile utilizzare RefCell<T>
nonostante i suoi
compromessi per ottenere più funzionalità rispetto a quelle fornite dai
reference standard.
Consentire più Proprietari di Dati Mutabili
Un modo comune per utilizzare RefCell<T>
è in combinazione con Rc<T>
.
Ricorda che Rc<T>
consente di avere più proprietari di alcuni dati, ma
fornisce solo un accesso immutabile a tali dati. Se hai un Rc<T>
che contiene
un RefCell<T>
, puoi ottenere un valore che può avere più proprietari e che
puoi mutare!
Ad esempio, ricorda l’esempio della cons list nel Listato 15-18, dove abbiamo
utilizzato 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 nell’elenco una volta creato. Aggiungiamo
RefCell<T>
per la sua capacità di modificare i valori negli elenchi. Il
Listato 15-24 mostra che utilizzando RefCell<T>
nella definizione di Cons
,
possiamo modificare il valore memorizzato in tutte le liste.
#[derive(Debug)] enum Lista { Cons(Rc<RefCell<i32>>, Rc<Lista>), Nil, } use crate::Lista::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; fn main() { let valore = Rc::new(RefCell::new(5)); let a = Rc::new(Cons(Rc::clone(&valore), 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)); *valore.borrow_mut() += 10; println!("a dopo = {a:?}"); println!("b dopo = {b:?}"); println!("c dopo = {c:?}"); }
Rc<RefCell<i32>>
per creare una Lista
che possiamo modificareCreiamo un valore che è un’istanza di Rc<RefCell<i32>>
e lo memorizziamo in
una variabile denominata valore
in modo da potervi accedere direttamente in
seguito. Quindi creiamo una Lista
in a
con una variante Cons
che contiene
valore
. Dobbiamo clonare valore
in modo che sia a
che valore
abbiano la
ownership del valore 5
interno, anziché trasferire la proprietà da valore
ad a
o far sì che a
prenda il prestito da valore
.
Racchiudiamo la lista a
in un Rc<T>
in modo che quando creiamo le liste b
e c
, possano entrambe fare riferimento ad a
, come abbiamo fatto nel Listato
15-18.
Dopo aver creato le liste in a
, b
e c
, vogliamo aggiungere 10 al valore in
valore
. Lo facciamo chiamando borrow_mut
su valore
, che utilizza la
funzione di de-referenziazione automatica di cui abbiamo parlato in “Dov’è
l’operatore ->
?” nel Capitolo 5 per
de-referenziare Rc<T>
al valore interno RefCell<T>
. Il metodo borrow_mut
restituisce un puntatore intelligente RefMut<T>
, su cui utilizziamo
l’operatore di de-referenziazione e modifichiamo il valore interno.
Quando stampiamo a
, b
e c
, possiamo vedere che hanno tutti il valore
modificato di 15
anziché 5
:
$ cargo run
Compiling cons-list v0.1.0 (file:///progetti/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 3.78s
Running `target/debug/cons-list`
a dopo = Cons(RefCell { value: 15 }, Nil)
b dopo = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c dopo = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
Questa tecnica è davvero interessante! Utilizzando RefCell<T>
, abbiamo un
valore Lista
esternamente immutabile. Ma possiamo usare i metodi su
RefCell<T>
che forniscono l’accesso alla sua mutabilità interna, così da poter
modificare i nostri dati quando necessario. I controlli durante l’esecuzione
delle regole di prestito ci proteggono dalle data race, e a volte vale la pena
sacrificare un po’ di prestazioni per questa flessibilità nelle nostre strutture
dati. Nota che RefCell<T>
non funziona per il codice multi-thread!
Mutex<T>
è la versione di RefCell<T>
che funzioni ai ambito multi-thread e
ne parleremo nel Capitolo 16.