Tipi di dato in Rust

Rust tipi di dato

In programmazione, ogni valore conservato in memoria è un tipo di dato, semplice o complesso. I tipi di dato definiti primitivi possono essere numeri o caratteri mentre gli oggetti sono considerati tipi di dato complessi. Proprio per la questione di sicurezza e dell’efficienza della memoria e per evitare errori in runtime, Rust richiede di conoscere i tipi di dato di tutte le variabili usate nel progetto in fase di compilazione.

Rust è in grado di dedurre il tipo di dato utilizzato in base al suo valore e al modo in cui viene visualizzato. In VS Code apparirà il tipo di dato dedotto da Rust scritto in grigio accanto ad ogni variabile: solo in rari casi ci verrà chiesto di indicare esplicitamente che tipo di dato rappresenta un valore. Questo perché non è possibile modificare il tipo di una variabile dopo averla creata nemmeno se questa è mutabile. Quindi se scrivo a = 3; Rust sa già che a è un intero e inserirà come tipo l’intero predefinito: i32. Se vogliamo modificare il tipo (es. i64), dobbiamo scriverlo esplicitamente e Rust accetterà il nuovo tipo.

Tipo scalari

In Rust i tipo scalari sono quelli che rappresentano un singolo valore e quelli principali sono in tutto quattro: interi, numeri decimali, valori booleani e caratteri.

Sia per i valori booleani che per i caratteri non è necessario dichiarare esplicitamente il tipo. I caratteri vanno inseriti dentro le virgolette singole, non le doppie come nelle stringhe. Un valore booleano occupa 1 byte mentre un carattere 4 byte di memoria e rappresenta un carattere UNICODE. Quindi possiamo inserire anche le emoji e ogni tipo di carattere speciale.

Numeri interi

Gli interi sono i numeri naturali con cui operiamo ogni giorno. Il valore predefinito è u32, cioè numeri interi senza segno a 32 bit.

Possiamo lavorare con interi a 8, 16, 32, 64 e 128 bit. Se aggiungiamo il prefisso u, allora i numeri sono sempre e solo  maggiori di 0 mentre con il prefisso i stiamo dicendo a Rust di operare anche con i numeri negativi.

Infine abbiamo i tipi usize e isize: questi si adattano all’architettura del dispositivo dell’utente e vengono utilizzati quando puntiamo a indici di memoria o per calcolare la lunghezza di una collezione di valori.

Ogni tipo di numero intero ha un suo valore massimo, calcolato dall’equazione 2n-1. Ad esempio gli interi a 8-bit senza segno hanno un valore massimo di 255 mentre quelli con segno possono avere un valore compreso tra -128 e 127. Quando aggiungiamo il segno un bit viene usato per il segno perciò in questo caso rimangono 7 bit per il numero.

Quando superiamo il valore massimo parliamo di overflow. In debug, Rust segnalerà l’errore ma se creiamo la release del progetto il numero viene convertito usando il complemento a due. Nel caso di u8, il numero 256 diventa 0, 257 diventa 1 e così via.

In generale è meglio correggere gli errori prima di rilasciare la release a meno che il codice richieda dei metodi specifici per non superare il valore massimo. Esempio: i punteggi di un videogioco.

Numeri decimali

I numeri decimali hanno possono essere scritti con o senza segno. Esistono due tipi: f32 e f64. Quest’ultimo è quello predefinito dato che i moderni PC hanno l’architettura a 64-bit. Devono sempre essere scritto in formato decimale come 2.0, 23.4.

Rust supporta le operazioni matematiche di base come addizione, sottrazione, moltiplicazione e divisione. Per calcolare il resto di una divisione usiamo l’operatore %.

Tipi composti

I tipi di dato che contengono più valori vengono chiamati tipi complessi. In Rust come tipi predefiniti abbiamo le tuple e gli array. Le differenze sono:

  • Le tuple possono contenere valori di tipo diverso, separate da virgole e dentro le parentesi tonde;
  • Gli array possono contenere solo valori dello stesso tipo, separate da virgole e dentro le parentesi quadre;

Tuple

Le tuple hanno una lunghezza fissa: dopo averne creata una, questa può contenere soltanto quei dati. Inoltre, se in una posizione indichiamo un tipo anche se modifichiamo il suo valore deve essere sempre dello stesso tipo.

Dato che una tupla è un tipo composto per accedere ai suoi valori dobbiamo destrutturarla e assegnare ciascun valore ad una variabile.

fn main() {
    let point = (10,20);

    let (x,y) = point;

    println!("x: {}", x);
    println!("y: {}", y);
}

Oppure possiamo accedere direttamente ai suoi indici:

fn main() {
    let point = (10,20);

    let x = point.0;
    let y = point.1;

    println!("x: {}", x);
    println!("y: {}", y);
}

Array

Anche gli array devo avere una lunghezza fissa. Questo è diverso rispetto agli altri linguaggi di programmazione sempre per la questione della gestione ottimale della memoria in fase di compilazione.

Quando creiamo un array possiamo inserire un solo valore predefinito, indicare la lunghezza, cioè quanti valori può contenere, e modificare il valore di ciascun campo successivamente.

fn main() {
    let array1 = [1,2,3,4,5];;

    let array2 = [1;5];

    println!("array1: {:?}",array1); //[1,2,3,4,5]
    println!("array2: {:?}",array2); //[1,2,3,4,5]
}

Il vantaggio di questo approccio è che possiamo dare un valore predefinito ad ogni campo e poi aggiungere quello che vogliamo successivamente. Ad esempio, in un videogioco possiamo avere la borsa vuota e poi aggiungere gli strumenti man mano che avanziamo.

    let mut bag = ["empty";20];

    bag[0] = "apple";

Oppure possiamo iterare l’array, cercare il primo campo vuoto e aggiungere lo strumento:

fn main() {

    let mut bag = ["empty";20];

    for i in &mut bag {
        if *i == "empty" {
            *i = "apple";
            break;
        }
    }

    println!("bag[0]:{}", bag[0]); 
}