|
Nel paragrafo precedente abbiamo detto che Rc<T> non gestisce la mutabilità. Ovvero non possiamo modificare o rendere modificabile un valore da questo puntato. Il seguente esempio commentato dimostrato quanto detto:
Se provate a togliere i delimitatori di commento e a compilare vederete comparire tanti splendidi errori. Anche de-referenziando, come si vede. Insomma, non c'è via di uscita. Tuttavia ci sono casi in cui le modifiche sono desiderate. Ed è qui che entra in campo RefCell<T>. Prima di proseguire precisiamo che anche in questo caso lavoriamo in ambito single-thread. La definizione formale è la seguente: pub struct RefCell where T: ?Sized, { /* private fields */ } La definizione è del tutto simile a quella di Rc, in pratica siamo davanti ad una struct generica che accetta qualsiasi tipo primitivo o complesso, come evidenziato da quel <T>, ormai dovrebbe essere chiaro. Come abbiamo visto nel paragrafo precedente la parte where T: ?Sized significa che non necessariamente l'elemento deve avere dimensioni note a compile time. Infine i campi sono privati. Per quanto le definizioni siano simili in realtà RefCell<T> implementa la cosiddetta interior mutability, concetto un po' complesso che magari ora è un po' troppo avanzato ma certamente non superfluo. Quindi proviamo a spiegarlo, seguendo la documentazione ufficiale, in pratica si tratta di una caratteristica di design del linguaggio che permette di modificare dei dati anche qualora vi siano dei riferimenti immutabili ad esso. I meccanismi interni sono abbastanza complicati, semplificando il discorso, si usa in questo caso il borrowing dinamico, ovvero una risorsa può richiedere temporaneamente accesso esclusivo al dato referenziato. Questo meccanismo si applica a runtime bypassando in qualche modo (ovvero lavorando in modalità unsafe) i controlli a compile time. Vediamo quindi l'esempio precedente opportunamente modificato:
Alla riga 1 richiamiamo la libreria necessaria e questo meccanismo è noto. Alla riga 3 creiamo una nuova istanza di RefCell che avvogliamo, per così dire intorno al valore 42 Alla riga 5 creiamo un riferimento immutabile al valore precedente. Possiamo creare, volendo, più riferimenti immutabili: let cell01 = RefCell::new(42); let val01 = cell01.borrow(); let val02 = cell01.borrow(); In questo caso il comportamento è simile a quello che si ha con Rc<T> salvo il fatto che non esiste, per quanto ne so, un contatore di riferimenti, ma solo un contatore interno che non è esponibile se non attraverso procedimenti di personalizzazione di RefCell. Essendo il riferimento creato alla riga 5 immutabile eliminare i delimitatori di commento alla 6 creerebbe panico nel programma, facile comprendere perchè. La documentazione ufficiale consiglia in casi dubbi di usare try_borrow. La riga 9 invece ci permette di usare direi tutta la forza di RefCell ovvero la possibilità di effettuare delle modifiche del valore referenziato. borrow_mut effettua un wrapper modificabile. Questo ci permette di effettuare delle modifiche nell'area di memoria puntata. Modifica che avviene alla riga 10. Possiamo creare più riferimenti mutabili? Risposta breve si. Risposta lunga... vediamo. Da un punto di vista sintattico il compilatore nulla eccepisce contro questo codice: use std::cell::RefCell; fn main() { let cell = RefCell::new(42); { let mut val = cell.borrow_mut(); let mut tal = cell.borrow_mut(); *val += 1; *tal += 2; } } però se dopo averlo compilato lo eseguite vi esce questo: thread 'main' panicked at r875.rs:6:28: already borrowed: BorrowMutError note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace r875.rs è il nome del file sorgente (creato da me, insomma), mentre la riga evidenziata ci dice cosa è successo: in pratica RefCell permette un solo prestito mutabile alla volta, il che se ci pensate un attimo è logico, pensate al caos che si creerebbe diversamente. Per far passare la cosa bisognerebbe usare drop, in forma esplicita, come ad esempio nel frammento di codice seguente: let mut val = cell.borrow_mut(); *val += 1; // Primo prestito mutabile drop(val); // Rilascia esplicitamente il prestito let mut tal = cell.borrow_mut(); *tal += 2; // Secondo prestito mutabile Prima di proseguire un piccolo suggerimemto che può essere utile. Se vogliamo estrarre il valore puntato da RefCell, come detto cerco sempre un approccio di tipo "pratico", dobbiamo procedere come segue in quanto questo costrutto non implementa Deref e quindi non basta usare il semplice operatore di dereferenziazione come nel caso di Rc. Vediamo il semplice codice seguente: use std::cell::RefCell; fn main() { let cell = RefCell::new(42); let value: i32 = *cell.borrow(); // De-referenziazione del wrapper Ref<T> println!("Il valore è: {}", value); } Bene, abbiamo visto nel capitolo precedente la possibilità di gestire riferimenti multipli ma immutabili e in questo riferimenti mutabili ma non moltiplicabili. E' il momento di unire le due cose. Vediamo subito l'esempio e di seguito lo commentiamo:
Quindi, usiamo entrambi come si evince dalle righe 1 e 2 che importano i moduli necessari. Alla riga 4 definiamo un Rc che contiene un RefCell che a sua volta contiene il valore 42 (ricordiamoci che entrambi hanno quel <T> generico nella loro definizione di base). Questo valore è modificabile in quanto, se vogliamo dirla facile, "puntato" tramite RefCell. Le righe 5 e 6 creano due nuovi riferimenti al valore definito alla riga 4 e quelle successive effettuano delle modifiche. Direi tutto sommato abbastanza semplice. Avrete notato forse una qualche somiglianza tra RefCell<T> e Box<T>, si tratta in sostanza di due wrapper che "si avvolgono" intorno ad una variabile. Sostanzialmente possiamo ricordare che: Box<T> è un puntatore intelligente che permette di allocare un valore sullo heap, mantenendo una singola proprietà del valore. Rust applica le sue regole di mutabilità e borrow-checking a compile-time per Box<T>. Caratteristiche di Box<T>:
RefCell<T>, invece, è progettato per situazioni in cui non è possibile determinare a compile-time se un valore sarà modificato. Consente di mutare un valore immutabile, sfruttando il pattern di interior mutability. Le regole di borrowing vengono applicate a runtime. Caratteristiche di RefCell<T>:
A questo punto non ci rimane che affrontare un ultimo problema ovvero il rischio di ritrovarci, usando Rc<T> con dei riferimenti circolari. Analizziamo ad esempio il codice seguente:
Tra le righe 5 e 8 creiamo una struct. Un campo è un intero, l'altro, next Rc che avvolge un RefCell il cui tipo puntato è una struttura dello stesso tipo in cui è definito. Questo ci serve per creare dei legami tra nodi. Alla riga 10 arriva il nostro main. Definiamo due nuovi nodi che, al momento della creazione nel campo next non hanno nulla. A questo punto colleghiamo node1 a node2 e viceversa. Questo avviene alle righe 23 e 25. Vale la pena notare che borrow_mut().next restituisce un Option oppure None se non esiste elemento successivo. O meglio:
Sopraggiunge a questo punto Weak<T> il quale crea un riferimento senza possesso all'elemento, appunto, un riferimento "debole". Per creare un semplice riferimento "debole" dobbiamo sottolineare che esso può essere creato solo a partire da un Rc. Ovvero: use std::rc::{Weak}; fn main() { let val : Weak } è un riferimento vuoto. Esistono due meccanismi che ovvero upgrade e downgrade. upgrade Tenta di convertire un Weak<T> in un Rc<T>. Restituisce:
downgrade Converte un Rc<T> in un Weak<T>, cioè un riferimento "debole" che:
Sulla base di questo diventa facile comprendere come creare un puntatore debole: use std::rc::{Rc, Weak}; fn main() { let rc_val = Rc::new(8); // Crea un Rc che punta a un intero let weak_val: Weak if let Some(strong_ref) = weak_val.upgrade() { println!("Il valore è: {}", strong_ref); } else { println!("Il valore è stato deallocato"); } } Il seguente esempio chiarirà meglio la dinamica ed il rapporto tra Rc e Weak:
La riga 3 crea un collegamento forte. A partire da esso alla 4 abbiamo la creazione di un riferimento debole. Quest'ultimo come detto garantisce il "puntamento" ma non il possesso per cui se vogliamo accedere alla validità dell'elemento da esso puntato, cioè della sua esistenza, dovremo ricorrere ad un upgrade, come alla riga 8, al fine di effettuare le verifiche, che avvengono sempre a partire dalla riga 8 e successivamente alla 16, anche in quel caso previo upgrade. Ora, viene spontanea una domanda: ma se Weak<T> crea un collegamento ma nel contempo non ha possesso dell'elemento, e quindi non ne impedisce la deallocazione non esiste il rischio di avere un puntatore ad un elemento non più esistente? Ricadendo nel famoso problema dei dangling pointer? In realtà abbiamo visto che per accedere ad un eventuale elemento è necessario effettuare un upgrade da cui si ricava un Option. Se quest'ultimo restituisce None significa che l'elemento è stato eliminato. In ogni caso non ne riceveremo dei dati casuali come succede in linguaggi come il C se non fate attenzione. L'utilità di Weak<T> ad ogni modo consiste nel fatto che pur creando un puntatore utilizzabile per ricavare un elemento puntato, non ne impedisce il rilascio. Vediamo ora l'esempio 42.3 rimodellato per l'uso con un riferimento debole (potete ingorare il warning relativo al dead code):
La presenza di un riferimento debole, definito alla riga 20, come evidenziato, permette le operazioni di dropping evitando quindi i riferimenti circolari dell'esempio precedente. Questo perchè sono i contatori "forti" che impediscono la deallocazione finché non arrivano a zero. In questo paragrafo abbiamo visto la costruzione di liste con riferimenti in Rust. Il codice è abbastanza complesso in effetti. Esiste uno specifico sito che tratta l'argomento delle liste in Rust: lettura molto interessante https://rust-unofficial.github.io/too-many-lists/index.html che consiglio caldamente di consultare quando avete voglia di immergervi in qualcosa di abbastanza complesso. Per concludere il capitolo, parliamo brevemente di try_borrow. Questo metodo viene molto utile qualora si voglia evitare il panic del programma cercando di usare RefCell su un elemento già puntato, come ricorderete RefCall non può essere applicato più di una volta sullo stesso elemento in forma mutabile. Vediamo l'esempio:
Alla riga 5 definiamo un primo prestito immutabile. Ne possiamo definire quanti ne vogliamo, lo sappiamo. Alla riga 12 però ne definiamo un mutabile quindi il tentativo alla riga 14 non può a questo punto andare a buon fine e l'output che viene presentato è quello della riga 18. Esiste anche try_borrow_mut() che prende un prestito mutabile, analogamente a quanto visto per borrow_mut(). |