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

Utilizzare Box<T> per Puntare ai Dati nell’Heap

Il puntatore intelligente più semplice è una box (scatola), il cui type è scritto Box<T>. Le box consentono di memorizzare i dati nell’heap anziché sullo stack. Ciò che rimane sullo stack è il puntatore ai dati nell’heap. Fai riferimento al Capitolo 4 per rinfrescare la memoria sulla differenza tra stack e heap.

Le box non hanno un overhead di prestazioni, a parte il fatto che memorizzano i dati nell’heap anziché sullo stack. Ma non hanno nemmeno molte funzionalità extra. Li userai più spesso in queste situazioni:

  • Quando hai un type la cui dimensione non può essere conosciuta in fase di compilazione e vuoi utilizzare un valore di quel type in un contesto che richiede una dimensione esatta
  • Quando hai una grande quantità di dati e vuoi trasferirne la ownership ma vuoi evitare che i dati vengano copiati quando lo fai
  • Quando vuoi possedere un valore e ti interessa solo che sia un type con un determinato trait piuttosto che essere di un type specifico

Descriveremo la prima situazione in “Abilitare i Type Ricorsivi con le Box. Nel secondo caso, il trasferimento della ownership di una grande quantità di dati può richiedere molto tempo perché i dati vengono copiati sullo stack. Per migliorare le prestazioni in questa situazione, possiamo memorizzare la grande quantità di dati nell’heap in una box. Quindi, solo la piccola quantità di dati del puntatore viene copiata sullo stack, mentre i dati a cui fa riferimento rimangono in un unico punto dell’heap. Il terzo caso è noto come oggetto trait (trait object), e la sezione “Usare gli Oggetti Trait per Astrarre Comportamenti Condivisi” nel Capitolo 18 è dedicata specificamente a questo argomento. Quindi, ciò che imparerai qui lo applicherai di nuovo in quella sezione!

Memorizzare Dati nell’Heap

Prima di discutere il caso d’uso di archiviazione nell’heap per Box<T>, tratteremo la sintassi e come interagire con i valori memorizzati all’interno di una Box<T>.

Il Listato 15-1 mostra come utilizzare una box per memorizzare un valore i32 nell’heap.

File: src/main.rs
fn main() {
    let b = Box::new(5);
    println!("b = {b}");
}
Listato 15-1: Memorizzare un valore i32 nell’heap tramite una box

Definiamo la variabile b come avente il valore di una Box che punta al valore 5, allocato nell’heap. Questo programma stamperà b = 5; in questo caso, possiamo accedere ai dati nella box in modo simile a come faremmo se questi dati fossero sullo stack. Proprio come qualsiasi valore posseduto, quando una box esce dallo scope, come accade a b alla fine di main, verrà de-allocata. La de-allocazione avviene sia per la box (memorizzata sullo stack) sia per i dati a cui punta (memorizzati nell’heap).

Mettere un singolo valore nell’heap non è molto utile, quindi le box non verranno utilizzate molto spesso da sole in questo modo. Avere valori come un singolo i32 sullo stack, dove vengono memorizzati di default, è più appropriato nella maggior parte delle situazioni. Diamo un’occhiata a un caso in cui le box ci consentono di definire type che non saremmo autorizzati a definire se non avessimo le box.

Abilitare i Type Ricorsivi con le Box

Un valore di un type ricorsivo (recursive type) può avere un altro valore dello stesso type come parte di sé. I type ricorsivi pongono un problema perché Rust deve sapere in fase di compilazione quanto spazio occupa un certo type. Tuttavia, l’annidamento dei valori dei type ricorsivi potrebbe teoricamente continuare all’infinito, quindi Rust non può sapere di quanto spazio ha bisogno il valore. Poiché le box hanno dimensioni note, possiamo abilitare i type ricorsivi inserendo una box nella definizione del type ricorsivo.

Come esempio di type ricorsivo, esploriamo la cons list (lista di costrutti). Questo è un tipo di dato comunemente presente nei linguaggi di programmazione funzionale. Il type di cons list che definiremo è semplice, fatta eccezione per la ricorsione; pertanto, i concetti nell’esempio con cui lavoreremo saranno utili ogni volta che ti troverai in situazioni più complesse che coinvolgono i type ricorsivi.

Comprendere la Cons List

Una Cons List è una struttura dati derivata dal linguaggio di programmazione Lisp e dai suoi dialetti, è composta da coppie annidate ed è la versione Lisp di una lista concatenata. Il suo nome deriva dalla funzione cons (abbreviazione di construct function) in Lisp, che costruisce una nuova coppia a partire dai suoi due argomenti. Chiamando cons su una coppia composta da un valore e un’altra coppia, possiamo costruire cons list composte da coppie ricorsive.

Ad esempio, ecco una rappresentazione in pseudo-codice di una cons list contenente la lista 1, 2, 3 con ciascuna coppia tra parentesi:

(1, (2, (3, Nil)))

Ogni elemento in una cons list contiene due elementi: il valore dell’elemento corrente e l’elemento successivo. L’ultimo elemento della lista contiene solo un valore chiamato Nil senza un elemento successivo. Una cons list viene prodotta chiamando ricorsivamente la funzione cons. Il nome canonico per indicare il caso base della ricorsione è Nil. Nota che questo non è lo stesso del concetto di “null” o “nil” discusso nel Capitolo 6, che indica un valore non valido o assente.

La cons list non è una struttura dati comunemente utilizzata in Rust. Nella maggior parte dei casi quando si ha una lista di elementi in Rust, Vec<T> è una scelta migliore. Altri tipi di dati ricorsivi più complessi sono utili in varie situazioni, ma iniziando con la cons list in questo capitolo, possiamo capire come le box ci consentano di definire un tipo di dati ricorsivo senza troppe distrazioni.

Il Listato 15-2 contiene una definizione enum per una cons list. Nota che questo codice non verrà ancora compilato perché il type Lista non ha una dimensione nota, che dimostreremo.

File: src/main.rs
enum Lista {
    Cons(i32, Lista),
    Nil,
}

fn main() {}
Listato 15-2: Il primo tentativo di definire una enum per rappresentare una struttura dati di tipo cons list di valori i32

Nota: stiamo implementando una cons list che contiene solo valori i32 per gli scopi di questo esempio. Avremmo potuto implementarla utilizzando i generici, come discusso nel Capitolo 10, per definire un tipo di cons list in grado di memorizzare valori di qualsiasi type.

L’utilizzo del type Lista per memorizzare l’elenco 1, 2, 3 sarebbe simile al codice nel Listato 15-3.

File: src/main.rs
enum Lista {
    Cons(i32, Lista),
    Nil,
}

// --taglio--

use crate::Lista::{Cons, Nil};

fn main() {
    let lista = Cons(1, Cons(2, Cons(3, Nil)));
}
Listato 15-3: Utilizzo dell’enum Lista per memorizzare la lista 1, 2, 3

Il primo valore Cons contiene 1 e un altro valore Lista. Questo valore Lista è un altro valore Cons che contiene 2 e un altro valore Lista. Questo valore Lista è un altro valore Cons che contiene 3 e un valore Lista, che è infine Nil, la variante non ricorsiva che segnala la fine della lista.

Se proviamo a compilare il codice nel Listato 15-3, otteniamo l’errore mostrato nel Listato 15-4.

$ cargo run
   Compiling cons-list v0.1.0 (file:///progetti/cons-list)
error[E0072]: recursive type `Lista` has infinite size
 --> src/main.rs:1:1
  |
1 | enum Lista {
  | ^^^^^^^^^^
2 |     Cons(i32, Lista),
  |               ----- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
2 |     Cons(i32, Box<Lista>),
  |               ++++     +

error[E0391]: cycle detected when computing when `Lista` needs drop
 --> src/main.rs:1:1
  |
1 | enum Lista {
  | ^^^^^^^^^^
  |
  = note: ...which immediately requires computing when `Lista` needs drop again
  = note: cycle used when computing whether `Lista` needs drop
  = note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information

Some errors have detailed explanations: E0072, E0391.
For more information about an error, try `rustc --explain E0072`.
error: could not compile `cons-list` (bin "cons-list") due to 2 previous errors
Listato 15-4: L’errore che otteniamo quando si tenta di definire una enum ricorsivo

L’errore indica che questo type “ha dimensione infinita”. Il motivo è che abbiamo definito Lista con una variante che è ricorsiva: contiene direttamente un altro valore di se stessa. Di conseguenza, Rust non riesce a calcolare quanto spazio è necessario per memorizzare un valore Lista. Analizziamo il motivo per cui otteniamo questo errore. Innanzitutto, vedremo come Rust determina quanto spazio è necessario per memorizzare un valore di un type non ricorsivo.

Calcolare la Dimensione di un Type Non Ricorsivo

Riprendiamo l’enum Messaggio che abbiamo definito nel Listato 6-2 quando abbiamo discusso le definizioni delle enum nel Capitolo 6:

enum Messaggio {
    Esci,
    Sposta { x: i32, y: i32 },
    Scrivi(String),
    CambiaColore(i32, i32, i32),
}

fn main() {}

Per determinare quanto spazio allocare per un valore Messaggio, Rust esamina ciascuna delle varianti per vedere quale variante necessita di più spazio. Rust vede che Messaggio::Esci non necessita di spazio, Messaggio::Sposta necessita di spazio sufficiente per memorizzare due valori i32 e così via. Poiché verrà utilizzata una sola variante, lo spazio massimo di cui un valore Messaggio avrà bisogno è lo spazio che richiederebbe per memorizzare la più grande delle sue varianti.

Confrontiamo questo con ciò che accade quando Rust cerca di determinare di quanto spazio necessita un type ricorsivo come l’enum Lista nel Listato 15-2. Il compilatore inizia esaminando la variante Cons, che contiene un valore di type i32 e un valore di type Lista. Pertanto, Cons necessita di una quantità di spazio pari alla dimensione di un i32 più la dimensione di un Lista. Per calcolare la quantità di memoria necessaria per il type Lista, il compilatore esamina le varianti, a partire dalla variante Cons. La variante Cons contiene un valore di type i32 e un valore di type Lista, e questo processo continua all’infinito, come mostrato nella Figura 15-1.

Una lista
_Cons_ infinita: un rettangolo etichettato 'Cons' diviso in due rettangoli più
piccoli. Il primo rettangolo più piccolo contiene l’etichetta 'i32', e il
secondo rettangolo più piccolo contiene l’etichetta 'Cons' e una versione più
piccola del rettangolo 'Cons' esterno. I rettangoli 'Cons' continuano a
contenere versioni sempre più piccole di se stessi finché il rettangolo più
piccolo, di dimensioni adeguate, contiene un simbolo di infinito, a indicare che
questa ripetizione continua all’infinito.

Figura 15-1: Una Lista infinita composta da infinite varianti Cons

Ottenere un Type Ricorsivo con una Dimensione Nota

Poiché Rust non riesce a calcolare quanto spazio allocare per i type definiti ricorsivamente, il compilatore genera un errore con questo utile suggerimento:

help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
2 |     Cons(i32, Box<Lista>),
  |               ++++     +

In questo suggerimento, indirection significa che invece di memorizzare un valore direttamente, dovremmo modificare la struttura dati per memorizzarlo indirettamente, memorizzando invece un puntatore al valore.

Poiché Box<T> è un puntatore, Rust sa sempre di quanto spazio una Box<T> necessita: la dimensione di un puntatore non cambia in base alla quantità di dati a cui punta. Questo significa che possiamo inserire Box<T> all’interno della variante Cons invece di un altro valore Lista direttamente. Box<T> punterà al successivo valore Lista che si troverà nell’heap anziché all’interno della variante Cons. Concettualmente, abbiamo ancora una lista, creata con liste che contengono altre liste, ma questa implementazione ora è più simile al posizionamento degli elementi uno accanto all’altro piuttosto che uno dentro l’altro.

Possiamo modificare la definizione dell’enum Lista nel Listato 15-2 e l’utilizzo di Lista nel Listato 15-3 con il codice nel Listato 15-5, che verrà compilato.

File: src/main.rs
enum Lista {
    Cons(i32, Box<Lista>),
    Nil,
}

use crate::Lista::{Cons, Nil};

fn main() {
    let lista = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
Listato 15-5: La definizione di Lista utilizzando Box<T> per avere una dimensione nota

La variante Cons richiede la dimensione di un i32 più lo spazio per memorizzare i dati del puntatore della box. La variante Nil non memorizza alcun valore, quindi necessita di meno spazio sullo stack rispetto alla variante Cons. Ora sappiamo che qualsiasi valore Lista occuperà le dimensioni di un i32 più le dimensioni dei dati del puntatore di una box. Utilizzando una box, abbiamo interrotto la catena infinita e ricorsiva, in modo che il compilatore possa calcolare la dimensione necessaria per memorizzare un valore Lista. La Figura 15-2 mostra l’aspetto attuale della variante Cons.

Un rettangolo etichettato
'Cons' diviso in due rettangoli più piccoli. Il primo rettangolo più piccolo
contiene l’etichetta 'i32', e il secondo rettangolo più piccolo contiene
l’etichetta 'Box' con un rettangolo interno che contiene l’etichetta 'usize',
che rappresenta la dimensione finita del puntatore della box.

Figura 15-2: Una Lista che non ha dimensioni infinite perché Cons contiene una Box

Le box forniscono solo l’indirezione e l’allocazione nell’heap; non hanno altre funzionalità speciali, come quelle che vedremo con gli altri tipi di puntatori intelligenti. Inoltre, non hanno alcun overhead prestazionale che queste funzionalità speciali comporterebbero, quindi possono essere utili in casi come la cons list, in cui l’indirezione è l’unica funzionalità di cui abbiamo bisogno. Esamineremo altri casi d’uso per le box nel Capitolo 18.

Il type Box<T> è un puntatore intelligente perché implementa il trait Deref, che consente di trattare i valori Box<T> come reference. Quando un valore Box<T> esce dallo scope, anche i dati dell’heap a cui punta il box vengono de-allocati grazie all’implementazione del trait Drop. Questi due trait saranno ancora più importanti per le funzionalità fornite dagli altri tipi di puntatore intelligente che discuteremo nel resto di questo capitolo. Vediamo questi due trait più in dettaglio.