SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI
Corso di Programmazione Rust Lezione 055 – Utilizzo di Box<T> per puntare ai dati sull’Heap
Utilizzo di Box<T> per Puntare a Dati nell’Heap
Il puntatore più diretto è una box, il cui tipo si scrive Box<T>. Le box ti consentono di memorizzare dati nell’heap invece che nello stack. Quello che rimane nello stack è il puntatore ai dati nell’heap. Consulta il Capitolo 4 per rivedere la differenza tra lo stack e l’heap.
Le box non hanno un overhead prestazionale, a parte il fatto di memorizzare i loro dati nell’heap invece che nello stack. Ma non hanno nemmeno molte capacità extra. Le userai più spesso in queste situazioni:
- Quando hai un tipo il cui dimensione non può essere conosciuta a tempo di compilazione e vuoi utilizzare un valore di quel tipo in un contesto che richiede una dimensione esatta.
- Quando hai una grande quantità di dati e vuoi trasferire la proprietà ma assicurarti che i dati non vengano copiati quando lo fai.
- Quando vuoi possedere un valore e ti interessa solo che sia un tipo che implementa un particolare trait anziché essere di un tipo specifico.
Dimostreremo la prima situazione nella sezione “Abilitare i Tipi Ricorsivi con le Box”. Nel secondo caso, il trasferimento della proprietà di una grande quantità di dati può richiedere molto tempo perché i dati vengono copiati nello 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 nello stack, mentre i dati a cui fa riferimento rimangono in un unico posto nell’heap. Il terzo caso è noto come un oggetto trait, e il Capitolo 17 dedica un’intera sezione, “Utilizzo degli Oggetti Trait Che Consentono Valori di Diversi Tipi”, solo a questo argomento. Quindi ciò che impari qui lo applicherai di nuovo nel Capitolo 17!
Utilizzare una Box<T> per Memorizzare Dati nell’Heap
Prima di discutere del caso d’uso di memorizzazione nell’heap per Box<T>, copriremo 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:
rust
fn main() {
let b = Box::new(5);
println!("b = {}", b);
}
Definiamo la variabile b per avere il valore di una Box che punta al valore 5, che è 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 nello stack. Proprio come qualsiasi valore posseduto, quando una box esce dallo scope, come fa b alla fine di main, verrà deallocata. La deallocazione avviene sia per la box (memorizzata nello stack) che per i dati a cui punta (memorizzati nell’heap).
Mettere un singolo valore nell’heap non è molto utile, quindi non userai spesso le box da sole in questo modo. Avere valori come un singolo i32 nello stack, dove sono memorizzati per impostazione predefinita, è più appropriato nella maggior parte delle situazioni. Vediamo un caso in cui le box ci consentono di definire tipi che non ci sarebbe permesso se non avessimo le box.
Abilitare i Tipi Ricorsivi con le Box
Un valore di tipo ricorsivo può avere un altro valore dello stesso tipo come parte di sé stesso. I tipi ricorsivi pongono un problema perché a tempo di compilazione Rust deve sapere quanto spazio occupa un tipo. Tuttavia, l’annidamento di valori di tipi ricorsivi potrebbe teoricamente continuare all’infinito, quindi Rust non può sapere quanto spazio ha bisogno il valore. Poiché le box hanno una dimensione nota, possiamo abilitare i tipi ricorsivi inserendo una box nella definizione del tipo ricorsivo.
Come esempio di un tipo ricorsivo, esploriamo la cons list. Questo è un tipo di dato comunemente trovato nei linguaggi di programmazione funzionale. Il tipo di cons list che definiremo è semplice tranne per la ricorsione; quindi, i concetti nell’esempio con cui lavoreremo saranno utili ogni volta che ci si imbatte in situazioni più complesse che coinvolgono tipi ricorsivi.
Calcolare le Dimensioni di un Tipo Non Ricorsivo
Ricordiamo l’enum Message che abbiamo definito nel Listato 6-2 quando abbiamo discusso le definizioni delle enum nel Capitolo 6:
rust
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
Per determinare quanto spazio allocare per un valore di tipo Message, Rust passa attraverso ciascuna delle varianti per vedere quale variante richiede più spazio. Rust vede che Message::Quit non ha bisogno di alcuno spazio, Message::Move ha bisogno dello spazio sufficiente per memorizzare due valori i32, e così via. Poiché verrà utilizzata solo una variante, lo spazio massimo necessario per un valore Message sarà lo spazio necessario per memorizzare la variante più grande.
Confrontiamo questo con ciò che accade quando Rust cerca di determinare quanto spazio necessita un tipo ricorsivo come l’enum List nel Listato 15-2. Il compilatore inizia esaminando la variante Cons, che contiene un valore di tipo i32 e un valore di tipo List. Pertanto, Cons ha bisogno di una quantità di spazio pari alla dimensione di un i32 più la dimensione di una List. Per capire quanta memoria necessita il tipo List, il compilatore guarda le varianti, iniziando con la variante Cons. La variante Cons contiene un valore di tipo i32 e un valore di tipo List, e questo processo continua all’infinito, come mostrato nella Figura 15-1.
Utilizzare Box<T> per Ottenere un Tipo Ricorsivo con una Dimensione Conosciuta
Poiché Rust non può capire quanto spazio allocare per tipi definiti in modo ricorsivo, il compilatore restituisce un errore con questo suggerimento utile:
go
help: inserire una certa indirezione (ad es., un `Box`, `Rc` o `&`) per rendere `List` rappresentabile
|
2 | Cons(i32, Box<List>),
| ++++ +
In questo suggerimento, “indirezione” significa che anziché memorizzare un valore direttamente, dovremmo modificare la struttura dati per memorizzare il valore in modo indiretto, memorizzando un puntatore al valore invece.
Poiché una Box<T> è un puntatore, Rust sa sempre quanto spazio ha bisogno una Box<T>: la dimensione di un puntatore non cambia in base alla quantità di dati a cui punta. Ciò significa che possiamo mettere una Box<T> all’interno della variante Cons anziché un altro valore List direttamente. La Box<T> punterà al prossimo valore List che sarà nell’heap anziché all’interno della variante Cons. Concettualmente, abbiamo comunque una lista, creata con liste che contengono altre liste, ma questa implementazione ora è più simile a posizionare gli elementi l’uno accanto all’altro anziché all’interno l’uno dell’altro.
Possiamo cambiare la definizione dell’enum List nel Listato 15-2 e l’uso del List nel Listato 15-3 al codice nel Listato 15-5, che compilerà:
rust
enum List {
Cons(i32, Box<List>),
Nil,
} use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
La variante Cons ha bisogno della dimensione di un i32 più lo spazio per memorizzare i dati del puntatore della box. La variante Nil non memorizza valori, quindi ha bisogno di meno spazio rispetto alla variante Cons. Ora sappiamo che qualsiasi valore List occuperà la dimensione di un i32 più la dimensione dei dati del puntatore di una box. Utilizzando una box, abbiamo interrotto la catena infinita e ricorsiva, quindi il compilatore può capire la dimensione necessaria per memorizzare un valore List. La Figura 15-2 mostra come appare ora la variante Cons.