Home Rhyylen
Contatto
 
 
 
Rust Language
Capitolo 41
RefCell<T> e Weak<T>

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:

  Esempio 42.1
1
2
3
4
5
6
7
8
9
10
11
12
use std::rc::Rc;

fn main() {
// Creiamo un Rc che punta a un numero intero
    let val = Rc::new(42);
// Proviamo a modificare il valore
// *Rc::get_mut(&mut val) = 100; // NON COMPILA
// Proviamo a de-referenziare e modificare
// *val = 100; // NON COMPILA
    println!("Valore immutabile: {}", val);
// L'unico modo per modificare è combinare Rc con un contenitore mutabile come RefCell
}

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<T>
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:

  Esempio 42.2
1
2
3
4
5
6
7
8
9
10
11
12
13
use std::cell::RefCell;
fn main() {
    let cell = RefCell::new(42);
    {
        let _val = cell.borrow(); // Prende un riferimento immutabile
        // let _mut_val = cell.borrow_mut(); // Questo causerebbe un panic a runtime
    }
    {
        let mut val = cell.borrow_mut(); // Prende un riferimento mutabile
        *val += 1; // Modifica il valore
    }
    println!("Valore aggiornato: {}", cell.borrow());
}

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:

  Esempio 42.2
1
2
3
4
5
6
7
8
9
10
11
12
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
    let valore = Rc::new(RefCell::new(42));
    let p1 = Rc::clone(&valore);
    let p2 = Rc::clone(&valore);
    *p1.borrow_mut() += 1;
    *p2.borrow_mut() += 2;
    println!("p1: {}", p1.borrow());
    println!("p2: {}", p2.borrow());
    println!("Conteggio dei riferimenti: {}", Rc::strong_count(&valore));
}

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>:

  • Unico proprietario: il valore contenuto è di proprietà esclusiva del Box<T>.
  • Mutabilità determinata staticamente: se dichiari il Box come mutabile (let mut box_val = Box::new(42)), puoi modificarne il valore; altrimenti no.
  • Nessuna mutabilità interna: il valore contenuto deve seguire le regole di borrowing standard di Rust.
mentre

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>:

  • Controllo a runtime: Rust verifica dinamicamente che ci sia un solo prestito mutabile o più prestiti immutabili attivi.
  • Mutabilità interna: puoi modificare un valore contenuto anche se l'oggetto esterno non è dichiarato come mutabile.

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:

 
  Esempio 42.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
value: i32,
next: Option<Rc<RefCell<Node>>>,
}

fn main() {
// Creiamo due nodi
  let node1 = Rc::new(RefCell::new(Node {
    value: 1,
    next: None,
  }));

  let node2 = Rc::new(RefCell::new(Node {
    value: 2,
    next: None,
  }));

// Creiamo un riferimento da node1 a node2
  node1.borrow_mut().next = Some(Rc::clone(&node2));

// Creiamo un riferimento circolare da node2 a node1
  node2.borrow_mut().next = Some(Rc::clone(&node1));

// Mostriamo il conteggio delle referenze
  println!("Conteggio node1: {}", Rc::strong_count(&node1)); // 2
  println!("Conteggio node2: {}", Rc::strong_count(&node2)); // 2

// Anche se i nodi escono dallo scope, non vengono mai deallocati
  println!("I nodi sono ancora in memoria!");
}

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:
  • Some(Rc<RefCell<Node>>) per un nodo successivo.
  • None se non esiste alcun nodo successivo.
il metodo clone crea quindi un altro puntatore verso i due nodi rispettivamente. Chiaramente non si può deallocare uno senza deallocare l'altro il che crea, alla fine, un memory leak in quanto la memoria rimane occupata anche gli elementi non servono più e quindi fino a quando, al termine del programma, il sistema operativo non spazza via tutto. 
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 = Weak::new();
}


è un riferimento vuoto. Esistono due meccanismi che ovvero upgrade e downgrade.

upgrade

Tenta di convertire un Weak<T> in un Rc<T>. Restituisce:

  • Some(Rc<T>) se il valore puntato è ancora valido.
  • None se il valore è stato già deallocato (perché non esistono più Rc<T> attivi).

downgrade

Converte un Rc<T> in un Weak<T>, cioè un riferimento "debole" che:

  • Non incrementa il contatore di riferimento forte (strong_count), evitando di bloccare la deallocazione della risorsa.
  • Può essere mantenuto per riferimenti opzionali o non critici.

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 = Rc::downgrade(&rc_val); // Crea un Weak a partire da Rc    
  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:

  Esempio 42.4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use std::rc::{Rc, Weak};
fn main() {
  let strong = Rc::new(42); // Riferimento forte
  let weak = Rc::downgrade(&strong); // Riferimento debole
  println!("Conteggio forte: {}", Rc::strong_count(&strong));
  println!("Conteggio debole: {}", Rc::weak_count(&strong));
  // Esegui un upgrade del Weak
  if let Some(value) = weak.upgrade() {
    println!("Valore disponibile: {}", value);
  }
  else {
    println!("Valore deallocato");
  }
  drop(strong); // Rimuovi l'ultimo riferimento forte
  // Dopo la deallocazione, l'upgrade fallisce
  if weak.upgrade().is_none() {
    println!("Valore deallocato, upgrade fallito");
  }
}

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):

  Esempio 42.5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
  value: i32,
  next: Option<Rc<RefCell<Node>>>,
  previous: Option<Weak<RefCell<Node>>>, // Debole per evitare riferimento circolare
}

fn main() {
  let node1 = Rc::new(RefCell::new(Node {
    value: 1,
    next: None,
    previous: None,
  }));

  let node2 = Rc::new(RefCell::new(Node {
  value: 2,
  next: None,
  previous: Some(Rc::downgrade(&node1)), // Riferimento debole
  }));

  node1.borrow_mut().next = Some(Rc::clone(&node2));

  println!("Conteggio node1: {}", Rc::strong_count(&node1)); // 2
  println!("Conteggio node2: {}", Rc::strong_count(&node2)); // 1
}

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:

  Esempio 42.6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use std::cell::RefCell;
fn main() {
    let cell = RefCell::new(42);
    // Proviamo a prendere in prestito il valore in modo immutabile
    let borrow = cell.try_borrow();
    if let Ok(borrow) = borrow {
        println!("Il valore è: {}", *borrow);
    } else {
        println!("Il valore è già preso in prestito in modo mutabile.");
    }
    // Prendiamo in prestito il valore in modo mutabile
    let _mut_borrow = cell.borrow_mut();
    // Proviamo a prendere in prestito di nuovo in modo immutabile
    let borrow = cell.try_borrow();
    if let Ok(borrow) = borrow {
        println!("Il valore è: {}", *borrow);
    } else {
        println!("Il valore è già preso in prestito in modo mutabile.");
    }
}

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().