Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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> e RefCell<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 di RefCell<T> anche quando RefCell<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.

File: src/lib.rs
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!");
        }
    }
}
Listato 15-20: Una libreria per tenere traccia di quanto un valore sia vicino a un valore massimo e avvisare quando il valore raggiunge determinati livelli

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.

File: src/lib.rs
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);
    }
}
Listato 15-21: Tentativo di implementare un MockMessaggero non consentito dal controllo dei prestiti

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

File: src/lib.rs
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);
    }
}
Listato 15-22: Usare RefCell<T> per modificare un valore interno mentre il valore esterno è considerato immutabile

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

File: src/lib.rs
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);
    }
}
Listato 15-23: Creazione di due reference mutabili nello stesso scope per verificare che RefCell<T> generi un panic

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

File: src/main.rs
#[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:?}");
}
Listato 15-24: Utilizzo di Rc<RefCell<i32>> per creare una Lista che possiamo modificare

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