Rust - Type system
Abbiamo detto nel capitolo precedente che in Rust ogni variabile ha un
tipo associato e con "tipo" a sua volta
si intende un insieme di valori coerenti, sostanzialmente. Il type system
di Rust è molto completo e, forse, un po' complesso. Il cammino che ci
aspetta infatti è piuttosto lungo e si dipanerà attraverso una tipologia
di dati peraltro piuttosto interessante.
Il type system di Rust è
moderno e statico, forte, espressivo e memory-safe, capace di unire la
sicurezza tipica dei linguaggi funzionali con le prestazioni di C/C++,
focalizzato sulla espressività ma soprattutto sulla sicurezza.
La suddivisione che
viene fatta sul sito ufficiale è la migliore di quelle che ho trovato e ad
essa mi rifaccio, evitando di reiventare l'acqua calda. Pertanto vediamo
le varie famiglie che riuniscono i tipi di dati disponibili seguendo lo
schema proposto in quella sede:
Tipi primitivi
--
Booleani
--
Numerici (numeri interi e con virgola)
-- Testuali (char e str)
-- Never -
un tipo senza valori - attualmente (marzo 2026) sperimentale e disponibile
solo nelle nightly.
Sequenze
-- Tuple (o ennuple)
-- Array
-- Slice
Tipi definiti dall'utente
-- Struct (o strutture)
-- Enum (o
enumerativi)
-- Union (o unione)
Tipi funzione
--
Funzioni
--
Chiusure
Puntatori
-- Riferimenti
-- Puntatori raw
-- Puntatori a
funzione
Trait
-- Oggetti trait
-- Impl trait
Come si vede l'elenco è lungo e, probabilmente non chiaro nelle sue
possibilità d'uso a questo punto del nostro percorso.
Ad ogni modo
cominceremo presto a vedere tutto passo dopo passo.
Prima di chiudere
vediamo un semplice programma di esempio che vi potrà essere utile per
determinare il tipo di una variabile a runtime. Prendete
tale programma così com'è senza farvi troppe domande:
Esempio 4.1
fn main() {
let x = 1;
let y = 1.3;
println!("{}", std::any::type_name_of_val(&x));
println!("{}", std::any::type_name_of_val(&y));
}
STACK E HEAP
Prima di proseguire un brevissimo richiamo a quelle strutture di memoria, lo stack e lo heap appunto, che vengono utilizzate dai compilatori moderni per memorizzare i dati dei programmi. Rust non fa eccezione e, per quanto la gestione di entrambi sia spesso del tutto trasparente al programmatore, è bene conoscerne l'esistenza e le peculiarità di base. E' una questione di cultura che prima o poi tornerà utile.
Iniziamo col dire, banale, lo so, che si tratta di due astrazioni: stack e heap sono aree di memoria create artificialmente dal compilatore, con la collaborazione del sistema operativo, non qualcosa di "fisico" nel computer. Si tratta quindi di particolari strutture che vengono create quando eseguite un programma e quindi eliminate quando questo termina.
Detto questo, vediamo le principali differenze in maniera molto sommaria, tenendo presente che anche in questo caso non vale la pena di reinventare l'acqua calda considerando la mole di materiale disponibile in rete sull'argomento:
Stack
- è più veloce dello heap, l'allocatore non ha bisogno di cercare spazio per mettere i dati in quanto la posizione è già definita in cima alla pila.
- l'allocazione dei dati avviene su blocchi contigui
- viene seguito uno schema LIFO, ovvero Last In First Out, l'esempio un po' banale ma ben descrittivo che si usa in questi casi è quello della classica pila di piatti.
- la deallocazione è immediata non appena il dato termina la sua vita
- dispone di meno spazio di archiviazione rispetto allo heap e può soffrire del problema noto come "stack overflow"
- effettua automatica la pulizia degli elementi non più utilizzati
Heap
- è dinamico per quanto riguarda l'allocazione e la deallocazione dei dati. Questo è certamente un vantaggio in quanto gli elementi in esso contenuti possono essere gestiti dinamicamente, ma si possono generare dei "buchi" nella struttura di quella zona di memoria che possono portare a spreco di risorse.
- dispone di spazio illimitato (o meglio limitato dalla RAM fisica) e non soffre di problemi di saturazione dello stesso
- l'accesso ai dati non è lineare come nello stack ma può essere casuale quindi più flessibile ma anche più delicato da gestire
- la pulizia degli oggetti non identificati è affidata al programmatore o a tecniche peculiari del compilatore adottato.
Di norma nello stack vengono memorizzati: parametri passati alla funzione, variabili locali (allocazione automatica), dati necessari a gestire la chiamata a funzione. Nello heap vanno le variabili di tipo riferimento, tipicamente gli oggetti. In generale un dato "certo" come un i32 andrà evidentemente nello stack. Ma un dato di cui non conosciamo le dimensioni e che può variare dinamicamente andrà nello heap.
Se volete un riassunto di confronto ecco la seguente tabella:
| Caratteristica | Stack | Heap |
|---|---|---|
| Tipo di allocazione | Automatica | Manuale o tramite GC |
| Gestione | LIFO (Last In, First Out) | Nessun ordine specifico |
| Velocità | Molto veloce | Più lenta |
| Dimensione | Limitata (dipende dal sistema) | Generalmente più ampia |
| Durata dei dati | Legata alla funzione (scope locale) | Persistente finché non viene liberata |
| Accesso | Diretto e semplice | Richiede puntatori |
| Rischi comuni | Stack overflow (raro ma possibile) | Memory leak, dangling pointer |
| Utilizzo tipico | Variabili locali, chiamate di funzione | Oggetti dinamici, strutture complesse |
ALIAS
Rust prevede un meccanismo di aliasing
chepermette di attribuire un nome alternativo a un tipo esistente,
solitamente per migliorare la leggibilità o ridurre la complessità del
codice. Si tratta di una pura funzionalità descrittiva che non crea un nuovo
tipo ne è in grado di alterare le funzionalità del tipo originale. Va usato
"cum grano salis" per evitare di ottenere l'effetto contrario, ovvero di
complicare la leggibilità del programma.
La sintassi è molto semplice e si basa sulla
keyword type:type NomeAlias = TipoEsistente;
per evitare un
warning da parte del compilatore l'alias deve seguire le consuete regole
CamelCase. Ed ecco un esempio:
Esempio 4.2
fn main() {
type Intero = i32;
let x1: Intero = 10;
let x2: Intero = 5;
println!("{}", x1 + x2);
}
Questo è un esempio di base ma è possibile usare l'aliasing anche per tipi complessi e puntatori.