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

Datatype - Tipi di dato

Ogni valore in Rust è di un determinato type, il che dice a Rust che tipo di dati vengono specificati in modo che sappia come lavorare con quei dati. Esamineremo due sottoinsiemi di tipi di dati: scalare e composto. Tieni presente che Rust è un linguaggio tipizzato staticamente, il che significa che deve conoscere il type di tutte le variabili in fase di compilazione. Il compilatore di solito può dedurre quale type vogliamo utilizzare in base al valore e al modo in cui lo utilizziamo. Nei casi in cui sono possibili molteplici type, come quando abbiamo convertito uno String in un type numerico usando parse nella sezione “Confrontare l’ipotesi con il numero segreto” del Capitolo 2, dobbiamo aggiungere un’annotazione, specificando il type in questo modo:

fn main() {
    let ipotesi: u32 = "42".parse().expect("Non è un numero!");
}

Se non aggiungiamo l’annotazione del type : u32 mostrata nel codice precedente, Rust visualizzerà il seguente errore, il che significa che il compilatore ha bisogno di ulteriori informazioni per sapere quale type vogliamo utilizzare:

$ cargo build
   Compiling annotazione_senza_type v0.1.0 (file:///progetti/annotazione_senza_type)

error[E0284]: type annotations needed
 --> src/main.rs:3:9
  |
3 |     let ipotesi = "42".parse().expect("Non è un numero!");
  |         ^^^^^^^        ----- type must be known at this point
  |
  = note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `ipotesi` an explicit type
  |
3 |     let ipotesi: /* Type */ = "42".parse().expect("Non è un numero!");
  |                ++++++++++++

For more information about this error, try `rustc --explain E0284`.
error: could not compile `annotazione_senza_type` (bin "annotazione_senza_type") due to 1 previous error; 1 warning emitted

Potrai vedere annotazioni di type diverso per altri tipi di dati.

I Type Scalari

Un type scalare rappresenta un singolo valore. Rust ha quattro type scalari primari: numeri interi, numeri in virgola mobile, booleani e caratteri. Potresti riconoscerli da altri linguaggi di programmazione. Andiamo a vedere come funzionano in Rust.

Type Intero

Un intero, integer d’ora in poi, è un numero senza una componente frazionaria. Nel Capitolo 2 abbiamo utilizzato un tipo integer, il type u32. Questa dichiarazione del type indica che il valore a cui è associato deve essere un integer senza segno (i type integer con segno iniziano con i invece che con u) che occupa 32 bit di spazio. La Tabella 3-1 mostra i type integer incorporati in Rust. Possiamo usare una qualsiasi di queste varianti per dichiarare il type di un valore intero.

Tabella 3-1: Type Integer in Rust

LunghezzaCon SegnoSenza Segno
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
in base all’architetturaisizeusize

Ogni variante può essere con segno o senza e ha una dimensione esplicita. Con segno e senza segno si riferisce alla possibilità che il numero sia negativo. In altre parole, se il numero deve avere un segno con sé (signed in inglese) o se sarà sempre e solo positivo e potrà quindi essere rappresentato senza segno (unsigned in inglese). È come scrivere numeri su carta: quando il segno conta, un numero viene indicato con il segno più o con il segno meno; tuttavia, quando è lecito ritenere che il numero sia positivo, viene visualizzato senza segno. I numeri con segno vengono memorizzati utilizzando la rappresentazione del complemento a due.

Ogni variante con segno può memorizzare numeri da -(2n - 1) a 2n - 1 - 1 inclusi, dove n è il numero di bit che la variante utilizza. Quindi un i8 può memorizzare numeri da -(27) a 27 - 1, il che equivale a -128 a 127. Le varianti senza segno possono memorizzare numeri da 0 a 2n - 1, quindi un u8 può memorizzare numeri da 0 a 28 - 1, il che equivale a 0 a 255.

Inoltre, i type isize e usize dipendono dall’architettura del computer su cui viene eseguito il programma: 64 bit se si tratta di un’architettura a 64 bit e 32 bit se si tratta di un’architettura a 32 bit.

Puoi scrivere i letterali integer in una qualsiasi delle forme mostrate nella Tabella 3-2. Nota che i letterali numerici che possono essere di più type numerici permettono, tramite un suffisso, di specificarne il type in questo modo 57u8. I letterali numerici possono anche usare _ come separatore visivo per rendere il numero più facile da leggere, come ad esempio 1_000, che avrà lo stesso valore che avrebbe se avessi specificato 1000.

Tabella 3-2: letterali Integer in Rust

Letterali numericiEsempio
Decimale98_222
Esadecimale0xff
Ottale0o77
Binario0b1111_0000
Byte (solo u8)b'A'

Come si fa a sapere quale type numerico utilizzare? Se non sei sicuro, le impostazioni predefinite di Rust sono in genere un buon punto di partenza: il type integer di default è i32. La situazione principale in cui puoi usare isize o usize è quando indicizzi qualche tipo di collezione.

Integer Overflow

Supponiamo di avere una variabile di type u8 che può contenere valori compresi tra 0 e 255. Se provi a cambiare la variabile con un valore al di fuori di questo intervallo, ad esempio 256, si verificherà un integer overflow, che può portare a uno dei due comportamenti seguenti. Quando stai compilando in modalità debug, Rust include controlli per l’integer overflow che fanno sì che il tuo programma vada in panico (panic d’ora in poi) in fase di esecuzione se si verifica questo comportamento. Rust usa il termine panic quando un programma termina con un errore; parleremo in modo più approfondito di panic nella sezione “Errori irrecuperabili con panic! nel Capitolo 9. Quando si compila in modalità release con il flag --release, Rust non include i controlli per l’overflow degli integer che causano il panic. Invece, se si verifica l’overflow, Rust esegue l’avvolgimento del complemento a due. In pratica, i valori maggiori del valore massimo che il type può contenere si “avvolgono” fino al minimo dei valori che il type può contenere. Nel caso di un u8, il valore 256 diventa 0, il valore 257 diventa 1 e così via. Il programma non andrà in panic, ma la variabile avrà un valore che probabilmente non è quello che ci si aspettava che avesse. Affidarsi all’avvolgimento del complemento a due degli integer è considerato un errore. Per gestire esplicitamente la possibilità di overflow, puoi utilizzare queste famiglie di metodi forniti dalla libreria standard per i type numerici primitivi:

  • Racchiudere tutte le modalità con i metodi wrapping_*, come ad esempio wrapping_add.
  • Restituire il valore None se c’è overflow con i metodi checked_*.
  • Restituire il valore e un booleano che indica se c’è stato overflow con i metodi overflowing_*.
  • Saturare i valori minimi o massimi del valore con i metodi saturating_*.

Type a Virgola Mobile

Rust ha anche due type primitivi per i numeri in virgola mobile, abbreviato float in inglese, che sono numeri con punti decimali. I type in virgola mobile di Rust sono f32 e f64, rispettivamente di 32 e 64 bit. Il tipo predefinito è f64 perché sulle CPU moderne ha più o meno la stessa velocità di f32 ma è in grado di garantire una maggiore precisione. Tutti i type in virgola mobile sono con segno.

Ecco un esempio che mostra i numeri in virgola mobile in azione:

File: src/main.rs

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

I numeri in virgola mobile sono rappresentati secondo lo standard IEEE-754.

Operazioni numeriche

Rust supporta le operazioni matematiche di base che ti aspetteresti per tutte le tipologie di numero: addizione, sottrazione, moltiplicazione, divisione e resto. La divisione degli interi tronca verso lo zero al numero intero più vicino. Il codice seguente mostra come utilizzare ogni operazione numerica in una dichiarazione let:

File: src/main.rs

fn main() {
    // addizione
    let somma = 5 + 10;

    // sottrazione
    let differenza = 95.5 - 4.3;

    // multiplicazione
    let prodotto = 4 * 30;

    // divisione
    let quoziente = 56.7 / 32.2;
    let troncato = -5 / 3; // Restituisce -1

    // resto
    let resto = 43 % 5;
}

Ogni espressione in queste dichiarazioni utilizza un operatore matematico e valuta un singolo valore, che viene poi legato a una variabile. Appendice B contiene un elenco di tutti gli operatori che Rust mette a disposizione.

Type Booleano

Come nella maggior parte degli altri linguaggi di programmazione, un type booleano in Rust ha due valori possibili: vero o falso (true e false rispettivamente d’ora in poi). I booleani hanno la dimensione di un byte. Il type booleano in Rust viene specificato con bool. Ad esempio:

File: src/main.rs

fn main() {
    let t = true;

    let f: bool = false; // con specificazione del type
}

Il modo principale per utilizzare i valori booleani è attraverso i condizionali, come ad esempio un’espressione if. Tratteremo il funzionamento delle espressioni if in Rust nella sezione “Controllo del flusso”.

Type Carattere

Il type carattere (char d’ora in poi) di Rust è il tipo alfabetico più primitivo del linguaggio. Ecco alcuni esempi di dichiarazione di valori char:

File: src/main.rs

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // con specificazione del type
    let gattino_innamorato = '😻';
}

Nota che specifichiamo i letterali char con le singole virgolette, al contrario dei letterali stringa, che utilizzano le virgolette doppie. Il tipo char di Rust ha la dimensione di quattro byte e rappresenta un valore scalare Unicode, il che significa che può rappresentare molte altre cose oltre all’ASCII. Le lettere accentate, i caratteri cinesi, giapponesi e coreani, le emoji e gli spazi a larghezza zero sono tutti valori char validi in Rust. I valori scalari Unicode vanno da U+0000 a U+D7FF e da U+E000 a U+10FFFF inclusi. Tuttavia, un “carattere” non è un concetto vero e proprio in Unicode, quindi quello che tu potresti concettualmente pensare essere un “carattere” potrebbe non corrispondere a cosa sia effettivamente un char in Rust. Discuteremo questo argomento in dettaglio in “Memorizzazione del testo codificato UTF-8 con le stringhe” nel Capitolo 8.

I Type Composti

I type composti possono raggruppare più valori in un unico type. Rust ha due type composti primitivi: le tuple e gli array.

Type Tupla

Una tupla è un modo generale per raggruppare una serie di valori di tipo diverso in un unico type composto. Le tuple hanno una lunghezza fissa: una volta dichiarate, non possono crescere o diminuire di dimensione. Creiamo una tupla scrivendo un elenco di valori separati da virgole all’interno di parentesi tonde. Ogni posizione nella tupla ha un type e i type dei diversi valori nella tupla non devono essere necessariamente gli stessi. In questo esempio abbiamo aggiunto annotazioni del type opzionali:

File: src/main.rs

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

La variabile tup si lega all’intera tupla perché una tupla è considerata un singolo elemento composto. Per ottenere i singoli valori di una tupla, possiamo fare pattern matching per destrutturare il valore di una tupla, in questo modo:

File: src/main.rs

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("Il valore di y è: {y}");
}

Questo programma crea prima una tupla e la associa alla variabile tup. Quindi utilizza un pattern con let per prendere tup e trasformarlo in tre variabili separate, x, y e z. Questa operazione è chiamata destrutturazione perché spezza la singola tupla in tre parti. Infine, il programma stampa il valore di y, che è 6,4.

Possiamo anche accedere direttamente a un elemento della tupla utilizzando un punto (.) seguito dall’indice del valore a cui vogliamo accedere. Ad esempio:

File: src/main.rs

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let cinque_cento = x.0;

    let sei_virgola_quattro = x.1;

    let uno = x.2;
}

Questo programma crea la tupla x e poi accede a ogni elemento della tupla utilizzando i rispettivi indici. Come nella maggior parte dei linguaggi di programmazione, il primo indice di una tupla è 0.

La tupla senza valori ha un nome speciale, unit. Questo valore e il suo type corrispondente sono entrambi scritti () e rappresentano un valore vuoto o un type di ritorno vuoto. Le espressioni restituiscono implicitamente il valore unit se non restituiscono nessun altro valore.

Type Array

Un altro modo per avere una collezione di valori multipli è un array. A differenza di una tupla, ogni elemento di un array deve avere lo stesso type. A differenza degli array in altri linguaggi, gli array in Rust hanno una lunghezza fissa.

Scriviamo i valori di un array come un elenco separato da virgole all’interno di parentesi quadre:

File: src/main.rs

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

Gli array sono utili quando vuoi che i tuoi dati siano allocati sullo stack, come gli altri type che abbiamo visto finora, piuttosto che sull’heap (parleremo dello stack e dell’heap in modo più approfondito nel Capitolo 4) o quando vuoi assicurarti di avere sempre un numero fisso di elementi. Un array, però, non è flessibile come il type vettore (vector d’ora in poi). Un vector è un type simile, che consente la collezione di dati, fornito dalla libreria standard ma che è autorizzato a crescere o a ridursi di dimensione perché il suo contenuto risiede sull’heap. Se non sei sicuro se usare un array o un vector, è probabile che tu debba usare un vector. Il Capitolo 8 tratta i vector in modo più dettagliato.

Tuttavia, gli array sono più utili quando sai che il numero di elementi non dovrà cambiare. Ad esempio, se dovessi utilizzare i nomi dei mesi in un programma, probabilmente utilizzeresti un array piuttosto che un vector perché sai che conterrà sempre 12 elementi:

#![allow(unused)]
fn main() {
let mesi = ["Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno",
            "Luglio", "Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"];
}

Il type array si scrive utilizzando le parentesi quadre con il type di ogni elemento, il punto e virgola e il numero di elementi dell’array, in questo modo:

#![allow(unused)]
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
}

In questo caso, i32 è il type di ogni elemento. Dopo il punto e virgola, il numero 5 indica che l’array contiene cinque elementi.

Puoi anche inizializzare un array in modo che contenga lo stesso valore per ogni elemento specificando il valore iniziale, seguito da un punto e virgola, e poi la lunghezza dell’array tra parentesi quadre, come mostrato qui:

#![allow(unused)]
fn main() {
let a = [3; 5];
}

L’array chiamato a conterrà 5 elementi che saranno tutti impostati inizialmente al valore 3. Questo equivale a scrivere let a = [3, 3, 3, 3, 3]; ma in modo più conciso.

Accesso agli elementi dell’array

Un array è un singolo blocco di memoria di dimensione fissa e nota che può essere allocato nello stack. Puoi accedere agli elementi di un array utilizzando l’indicizzazione, in questo modo:

File: src/main.rs

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

    let primo = a[0];
    let secondo = a[1];
}

In questo esempio, la variabile denominata primo otterrà il valore 1 perché è il valore all’indice [0] dell’array. La variabile denominata secondo otterrà il valore 2 dall’indice [1] dell’array.

Accesso all’elemento dell’array non valido

Vediamo cosa succede se cerchi di accedere a un elemento di un array che si trova oltre la fine dell’array stesso. Supponiamo di eseguire questo codice, simile al gioco di indovinelli del Capitolo 2, per ottenere un indice dell’array dall’utente:

File: src/main.rs

use std::io;

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

    println!("Digita un indice dell'array.");

    let mut indice = String::new();

    io::stdin()
        .read_line(&mut indice)
        .expect("Errore di lettura");

    let indice: usize = indice
        .trim()
        .parse()
        .expect("L'indice inserito non è un numero");

    let elemento = a[indice];

    println!("Il valore dell'elemento all'indice {indice} è: {elemento}");
}

Se esegui questo codice utilizzando cargo run e inserisci 0, 1, 2, 3 o 4, il programma stamperà il valore corrispondente a quell’indice nell’array. Se invece inserisci un numero oltre la fine dell’array, come ad esempio 10, vedrai un risultato come questo:

thread 'main' panicked at src/main.rs:19:20:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Il programma ha generato un errore durante l’esecuzione (at runtime in inglese) nel momento in cui ha utilizzato un valore non valido nell’operazione di indicizzazione. Il programma è uscito con un messaggio di errore e non ha eseguito l’istruzione finale println!. Quando si tenta di accedere a un elemento utilizzando l’indicizzazione, Rust controlla che l’indice specificato sia inferiore alla lunghezza dell’array. Se l’indice è maggiore o uguale alla lunghezza, Rust va in panic. Questo controllo deve avvenire in runtime, soprattutto in questo caso, perché il compilatore non può sapere quale valore inserirà l’utente quando eseguirà il codice in seguito.

Questo è un esempio dei principi di sicurezza della memoria di Rust in azione. In molti linguaggi di basso livello, questo tipo di controllo non viene fatto e quando si fornisce un indice errato, si può accedere a una memoria non valida. Rust ti protegge da questo tipo di errore uscendo immediatamente invece di consentire l’accesso alla memoria e continuare. Il Capitolo 9 tratta di altri aspetti della gestione degli errori di Rust e di come puoi scrivere codice leggibile e sicuro che non va in panic né consente l’accesso non valido alla memoria.