Home Rhyylen
Contatto
 
 
 
Rust Language
Capitolo 35
Boxing

Una parentesi la merita questo meccanismo di cui abbiamo accennato già in qualche paragrafo precedente.
Una definizione comune è la seguente: il meccanismo di box permette di allocare dinamicamente dei valori sullo heap e gestirne la deallocazione in modo sicuro. In pratica Box<T> è una scrittura che causa la creazione di un puntatore ad un elemento che si trova sullo heap. Viene utilizzato per creare tipi di dati che possiedono la proprietà di ownership il che può essere importante in alcune situazioni come vedremo tra breve. La prima cosa da capire è in quali scenari può essere utile ricorrere ad esso, a titolo esemplificativo: 
  • il caso più tipico è quello in cui la dimensione del tipo non sia noto a compile time
  • un altra situazione, non frequentissima, è quando si voglia alleggerire di dati lo stack
  • quando si voglia trasferire l'ownership di una grande quantita di dati evitandone la copia
  Un esempio di base che trovate ovunque  è il seguente:

  Esempio 35.1
1
2
3
4
fn main() {
    let b = Box::new(1);
    println!("b = {}", b);
}

Questo semplice programma dichiara una nuova variabile di tipo intero che normalmente sarebbe ospitata sullo stack. A causa dell'istruzione Box essa viene allocata sullo heap, sullo stack resta un puntatore (che ha dimensione fissa e quindi nota al compilatore) verso l'indrizzo nello heap che contiene il dato e al termine del suo scope, che in questo caso coincide col termine del programma, verrà cancellata dallo stesso heap e verrà eliminato il suo riferimento dallo stack.
Complichiamo un po' l'esempio precedente:

Esempio 35.2
1
2
3
4
5
6
7
8
9
fn main() {
    let b = Box::new(5);
    println!("b = {}", b);
    println!("{}", f(b));
}

fn f(x: i32) -> i32{
    return x * 5;
}

Questo codice non compila e, come sempre, l'errore che ci segnala il compilatore è molto esplicativo:

error[E0308]: mismatched types
--> r505.rs:4:22
|
4 | println!("{}", f(b));
| - ^ expected `i32`, found `Box<{integer}>`
| |
| arguments to this function are incorrect
|
= note: expected type `i32`
found struct `Box<{integer}>`
note: function defined here
--> r505.rs:7:4
|
7 | fn f(x: i32) -> i32{
| ^ ------
help: consider unboxing the value
|
4 | println!("{}", f(*b));
|

In pratica stiamo passando il tipo sbagliato, un valore boxato, permettetemi il brutto neologismo, è diverso da un intero, che tra l'altro dovrebbe stare sullo stack. Una soluzione è quella proposta, ovvero fare una operazione di unboxing riscrivendo la riga 4 come suggerito:

println!("{}", f(*b));

che è la via più semplice. Se volete esiste un'altra strada, che lascia invariata la riga 4 ma modifica pesantemente la funzione:

fn f(x: Box<i32>) -> i32{
return *x * 5;
}


con la consueta dereferenziazione per accedere direttamente all'intero.

Vediamo ora di approfondire un po' il discorso. Si potrebbe pensare che il meccanismo di boxing sia in sostanza un puntatore. Il che è vero, d'altronde possiamo usare la dereferenziazione con * esattamente come con i puntatori. Si parla però in questo caso di puntatori smart in quanto fanno qualche cosa di più dei puntatori normali introdotti da &. Una cosa interessante che è possibile fare tramite Box prende il nome di ricorsione. Un esempio banalissimo, ma che rende bene l'idea nella sua semplicità, sposandosi tra l'altro con la considerazione fatta all'inizio, quando abbiamo evidenziato che tramite questo meccanismo si può dare contribuire ad evitare gli stack overflow, è il seguente:

  Esempio 35.3
1
2
3
4
5
6
7
8
9
10
11
12
fn fattoriale(n: u64) -> Box<u64> {
    if n == 0 {
        Box::new(1)
    } else {
        Box::new(*fattoriale(n - 1) * n)
    }
}

fn main() {
    let num = 5;
    println!("Fattoriale di {} = {}", num, *fattoriale(num));
}

In questo esempio, la funzione fattoriale chiama se stessa ricorsivamente per calcolare il fattoriale di un numero. L’uso di Box permette di evitare problemi di dimensione dello stack perché il valore di ritorno viene allocato sul heap. Quando si chiama fattoriale(n - 1), il risultato viene dereferenziato con * per moltiplicarlo per n, e poi il risultato viene nuovamente messo in un Box prima di essere restituito.
Questo è un esempio di ricorsione un po' banale, pur se utile, diverso invece è il seguente:

  Esempio 35.4
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
struct Node {
    value: i32,
    next: Option<Box<Node>>,
}

impl Node {
    fn new(value: i32) -> Node {
        Node { value, next: None }
}

fn append(&mut self, value: i32) {
    let new_node = Box::new(Node::new(value));
    match self.next {
        Some(ref mut next_node) => next_node.append(value),
        None => self.next = Some(new_node),
        }
    }
}

fn main() {
    let mut start = Node::new(1);
    start.append(2);
    start.append(3);

// Stampa dei valori:
    let mut current = &start;
    while let Some(ref next) = current.next {
        println!("{}", current.value);
        current = next;
        }
    println!("{}", current.value); // Stampa anche l'ultimo valore
}

Definiamo una struct che oltre ad un valore numero ha un campo Option che punta ad un ulteriore nodo.
Alla riga 6 troviamo la funzione della struttura stessa che crea un nuovo nodo. La riga 21 definisce un nuovo nodo che porta il valore 1 e None nell'altro campo. Quando alla riga 22 creiamo un altro elemento interviene la riga 15, in cui il campo viene modificato e da None punta all'elemento successivo. In pratica la lista si presenta così:

dopo la riga 21:
Node { value: 2, next: None }
dopo la riga 22:
Node { value: 1, next: Some(Box(Node { value: 2, next: None })) }
e così via.
La riga 14 serve ovviamente per appendere elementi successivi richiamando ricorsivamente append quando il campo Option non è None.

Questo esempio mostra la potenza e l'utilità di Box ma non è così, come dire stringente, per invogliare davvero l'uso di esso, anche se non va dimenticata la grande forza del meccanismo di ricorsione. La grande forza di Box la vedremo quando parleremo in maniera approfondita dei trait. In realtà vi sono anche altri esempi applicativi utili con Box ma li vedremo più avanti.