Home Rhyylen
Contatto
 
 
 
Rust Language
Capitolo 3
Tipi, stack e heap

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 piuttosto interessante. 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 (aprile 2024) 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 3.1
1
2
3
4
5
6
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));
}

abbiamo preso ad esempio due numeri ma il metodo è applicabile anche su altri tipi.


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

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 3.2
1
2
3
4
5
6
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.