SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI
Tipi di Dati
Ogni valore in Rust è di un certo tipo di dati, che dice a Rust che tipo di dati è stato specificato in modo che sappia come lavorare con quei dati. Esamineremo due sottoinsiemi di tipi di dati: scalar e compound.
Tieni presente che Rust è un linguaggio staticamente tipizzato, il che significa che deve conoscere i tipi di tutte le variabili durante il tempo di compilazione. Il compilatore può solitamente inferire il tipo che vogliamo usare in base al valore e a come lo usiamo. Nei casi in cui sono possibili molti tipi, come quando abbiamo convertito una Stringa in un tipo numerico usando parse
nella sezione “Confrontare il Tentativo con il Numero Segreto” nel Capitolo 2, dobbiamo aggiungere una annotazione di tipo, come questo:
rust
let guess: u32 = "42".parse().expect("Non è un numero!");
Se non aggiungiamo l’annotazione di tipo : u32
mostrata nel codice precedente, Rust mostrerà il seguente errore, il che significa che il compilatore ha bisogno di più informazioni da noi per sapere quale tipo vogliamo usare:
less
$ cargo build
Compilazione di no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
errore[E0282]: servono annotazioni di tipo
--> src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Non è un numero!");
| ^^^^^
|
aiuto: considera di dare a `guess` un tipo esplicito
|
2 | let guess: _ = "42".parse().expect("Non è un numero!");
| +++
Per ulteriori informazioni su questo errore, prova `rustc --explain E0282`.
errore: impossibile compilare `no_type_annotations` a causa di un errore precedente
Vedrai diverse annotazioni di tipo per altri tipi di dati.
Tipi Scalar
Un tipo scalar rappresenta un singolo valore. Rust ha quattro tipi scalar principali: interi, numeri in virgola mobile, booleani e caratteri. Potresti riconoscerli da altri linguaggi di programmazione. Vediamo come funzionano in Rust.
Tipi Interi
Un intero è un numero senza una componente frazionaria. Abbiamo usato un tipo intero nel Capitolo 2, il tipo u32. Questa dichiarazione di tipo indica che il valore associato dovrebbe essere un intero non firmato (i tipi di interi con segno iniziano con i invece di u) che occupa 32 bit di spazio. La Tabella 3-1 mostra i tipi di interi incorporati in Rust. Possiamo usare una qualsiasi di queste varianti per dichiarare il tipo di un valore intero.
Tabella 3-1: Tipi Interi in Rust Lunghezza Firmato Non Firmato 8 bit i8 u8 16 bit i16 u16 32 bit i32 u32 64 bit i64 u64 128 bit i128 u128 arch isize usize
Ogni variante può essere sia firmata che non firmata e ha una dimensione esplicita. Firmato e non firmato si riferiscono alla possibilità che il numero sia negativo – in altre parole, se il numero deve avere un segno con esso (firmato) o se sarà solo positivo e quindi può essere rappresentato senza un segno (non firmato). È come scrivere numeri su carta: quando il segno è importante, un numero viene mostrato con un segno più o meno; tuttavia, quando è sicuro presumere che il numero sia positivo, viene mostrato senza un segno. I numeri firmati sono memorizzati utilizzando la rappresentazione a complemento a due.
Ogni variante firmata può memorizzare numeri da -(2^n – 1) a 2^n – 1 – 1 inclusi, dove n è il numero di bit che la variante utilizza. Quindi un i8 può memorizzare numeri da -(2^7) a 2^7 – 1, che equivale a -128 a 127. Le varianti non firmate possono memorizzare numeri da 0 a 2^n – 1, quindi un u8 può memorizzare numeri da 0 a 2^8 – 1, che equivale a 0 a 255.
Inoltre, i tipi isize e usize dipendono dall’architettura del computer su cui viene eseguito il tuo programma, che è indicato nella tabella come “arch”: 64 bit se sei su un’architettura a 64 bit e 32 bit se sei su un’architettura a 32 bit.
Puoi scrivere letterali interi in una qualsiasi delle forme mostrate nella Tabella 3-2. Nota che i letterali numerici che possono essere di più tipi numerici consentono un suffisso di tipo, come 57u8, per designare il tipo. I letterali numerici possono anche usare _ come separatore visivo per rendere il numero più facile da leggere, come 1_000, che avrà lo stesso valore se avessi specificato 1000.
Tabella 3-2: Letterali Interi in Rust Letterali numerici Esempio Decimale 98_222 Esadecimale 0xff Ottale 0o77 Binario 0b1111_0000 Byte (solo u8) b’A’
Quindi come fai a sapere quale tipo di intero usare? Se non sei sicuro, i valori predefiniti di Rust sono generalmente buoni punti di partenza: i tipi di interi predefiniti sono i32. La situazione principale in cui useresti isize o usize è quando indicizzi qualche tipo di raccolta.
Overflow degli Interi
Supponiamo di avere una variabile di tipo u8 che può contenere valori tra 0 e 255. Se provi a cambiare la variabile a un valore al di fuori di quel range, come 256, si verificherà un overflow degli interi, che può comportare uno dei due comportamenti. Quando compili in modalità debug, Rust include controlli per overflow degli interi che fanno uscire il tuo programma in runtime se si verifica questo comportamento. Rust usa il termine “panicking” quando un programma esce con un errore; parleremo di panics più approfonditamente nella sezione “Errori Irrecuperabili con panic!” nel Capitolo 9.
Quando compili in modalità di rilascio con il flag –release, Rust non include controlli per overflow degli interi che causano panics. Invece, se si verifica un overflow, Rust esegue l’avvolgimento in complemento a due. In breve, i valori maggiori del valore massimo che il tipo può contenere “si avvolgono” al minimo dei valori che il tipo può contenere. Nel caso di un u8, il valore 256 diventa 0, il valore 257 diventa 1, e così via. Il programma non farà panic, ma la variabile avrà un valore che probabilmente non è quello che ti aspettavi. Fare affidamento sul comportamento di avvolgimento dell’overflow degli interi è considerato un errore.
Per gestire esplicitamente la possibilità di overflow, puoi utilizzare queste famiglie di metodi forniti dalla libreria standard per i tipi numerici primitivi:
- Avvolgi in tutti i modi con i metodi wrapping_*, come wrapping_add.
- Restituisci il valore None se c’è overflow con i metodi checked_*.
- Restituisci il valore e un booleano che indica se c’è stato overflow con i metodi overflowing_*.
- Satura ai valori minimo o massimo del valore con i metodi saturating_*.
Tipi in Virgola Mobile
Rust ha anche due tipi primitivi per i numeri in virgola mobile, che sono numeri con punti decimali. I tipi in virgola mobile di Rust sono f32 e f64, che sono rispettivamente di 32 bit e 64 bit in dimensione. Il tipo predefinito è f64 perché sui processori moderni, è approssimativamente alla stessa velocità di f32 ma è in grado di avere più precisione. Tutti i tipi in virgola mobile sono firmati.
Ecco un esempio che mostra i numeri in virgola mobile in azione:
Nome file: src/main.rs
rust
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. Il tipo f32 è un float a singola precisione, e f64 ha doppia precisione.
Operazioni Numeriche
Rust supporta le operazioni matematiche di base che ti aspetti per tutti i tipi di numeri: addizione, sottrazione, moltiplicazione, divisione e resto. La seguente codice mostra come usare ciascuna operazione numerica in uno statement let:
Nome file: src/main.rs
rust
fn main() {
// addizione
let sum = 5 + 10; // sottrazione
let difference = 95.5 - 4.3; // moltiplicazione
let product = 4 * 30; // divisione
let quotient = 56.7 / 32.2;
let truncated = -5 / 3; // Risultato -1
// resto
let remainder = 43 % 5;
}
Ogni espressione in questi statement usa un operatore matematico e si valuta a un singolo valore, che viene quindi vincolato a una variabile. L’Appendice B contiene un elenco di tutti gli operatori che Rust fornisce.
Il Tipo Booleano
Come nella maggior parte degli altri linguaggi di programmazione, un tipo booleano in Rust ha due possibili valori: true e false. I booleani occupano un byte di dimensione. Il tipo booleano in Rust è specificato usando bool. Ad esempio:
Nome file: src/main.rs
rust
fn main() {
let t = true;
let f: bool = false; // con annotazione di tipo esplicito
}
Il modo principale per utilizzare i valori booleani è attraverso condizioni, come ad esempio un’espressione if. Vedremo come funzionano le espressioni if in Rust nella sezione “Controllo del Flusso”.
Il Tipo Carattere
Il tipo char di Rust è il tipo alfabetico più primitivo del linguaggio. Ecco alcuni esempi di dichiarare valori char:
Nome file: src/main.rs
rust
fn main() {
let c = 'z';
let z: char = 'ℤ'; // con annotazione di tipo esplicito
let heart_eyed_cat = '😻';
}
Nota che specifichiamo i letterali char con singoli apici, a differenza dei letterali stringa, che usano doppi apici. Il tipo char di Rust ha una dimensione di quattro byte e rappresenta un valore Unicode Scalar, il che significa che può rappresentare molto più che solo ASCII. Lettere accentate; caratteri cinesi, giapponesi e coreani; emoji; e spazi di larghezza zero sono tutti valori char validi in Rust. I valori Unicode Scalar vanno da U+0000 a U+D7FF e da U+E000 a U+10FFFF inclusi. Tuttavia, un “carattere” non è davvero un concetto in Unicode, quindi la tua intuizione umana su cosa sia un “carattere” potrebbe non coincidere con ciò che è un char in Rust. Discuteremo questo argomento in dettaglio in “Memorizzazione di Testo Codificato in UTF-8 con Stringhe” nel Capitolo 8.
Tipi Compositi
I tipi compositi possono raggruppare più valori in un unico tipo. Rust ha due tipi compositi primitivi: tuple e array.
Il Tipo Tuple
Una tupla è un modo generale per raggruppare insieme un numero di valori con una varietà di tipi in un unico tipo composto. Le tuple hanno una lunghezza fissa: una volta dichiarate, non possono crescere o diminuire di dimensione.
Creiamo una tupla scrivendo una lista separata da virgole di valori tra parentesi. Ogni posizione nella tupla ha un tipo, e i tipi dei diversi valori nella tupla non devono essere gli stessi. Abbiamo aggiunto annotazioni di tipo opzionali in questo esempio:
Nome file: src/main.rs
rust
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 valori individuali da una tupla, possiamo usare il pattern matching per destrutturare un valore di tupla, così:
Nome file: src/main.rs
rust
fn main() {
let tup = (500, 6.4, 1); let (x, y, z) = tup;
println!("Il valore di y è: {y}");
}
Questo programma prima crea una tupla e la lega alla variabile tup. Quindi usa un pattern con let per prendere tup e trasformarlo in tre variabili separate, x, y e z. Questo è chiamato 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 di una tupla usando un punto (.) seguito dall’indice del valore che vogliamo accedere. Ad esempio:
Nome file: src/main.rs
rust
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1); let five_hundred = x.0; let six_point_four = x.1;
let one = x.2;
}
Questo programma crea la tupla x e quindi accede a ciascun elemento della tupla usando i loro rispettivi indici. Come con la maggior parte dei linguaggi di programmazione, il primo indice in una tupla è 0.
La tupla senza alcun valore ha un nome speciale, unit. Questo valore e il suo tipo corrispondente sono entrambi scritti () e rappresentano un valore vuoto o un tipo di ritorno vuoto. Le espressioni restituiscono implicitamente il valore di unità se non restituiscono alcun altro valore.
Il Tipo Array
Un altro modo per avere una raccolta di valori multipli è con un array. A differenza di una tupla, ogni elemento di un array deve avere lo stesso tipo. A differenza degli array in alcuni altri linguaggi, gli array in Rust hanno una lunghezza fissa.
Scriviamo i valori in un array come una lista separata da virgole all’interno di parentesi quadre:
Nome file: src/main.rs
rust
fn main() {
let a = [1, 2, 3, 4, 5];
}
Gli array sono utili quando vuoi che i tuoi dati siano allocati nello stack invece che nello heap (discuteremo dello stack e dello heap più approfonditamente nel Capitolo 4) o quando vuoi garantire di avere sempre un numero fisso di elementi. Un array non è così flessibile del tipo vettore, però. Un vettore è un tipo di raccolta simile fornito dalla libreria standard che è autorizzato a crescere o diminuire di dimensione. Se non sei sicuro se usare un array o un vettore, probabilmente dovresti usare un vettore. Il Capitolo 8 discute i vettori in modo più dettagliato.
Tuttavia, gli array sono più utili quando sai che il numero di elementi non dovrà cambiare. Ad esempio, se stavi usando i nomi dei mesi in un programma, probabilmente useresti un array invece di un vettore perché sai che conterrà sempre 12 elementi:
rust
let months = ["Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno", "Luglio",
"Agosto", "Settembre", "Ottobre", "Novembre", "Dicembre"];
Scrivi il tipo di un array usando parentesi quadre con il tipo di ciascun elemento, un punto e virgola e quindi il numero di elementi nell’array, così:
rust
let a: [i32; 5] = [1, 2, 3, 4, 5];
Qui, i32 è il tipo di ogni elemento. Dopo il punto e virgola, il numero 5 indica che l’array contiene cinque elementi.
Puoi anche inizializzare un array per contenere lo stesso valore per ogni elemento specificando il valore iniziale, seguito da un punto e virgola e quindi la lunghezza dell’array tra parentesi quadre, come mostrato qui:
rust
let a = [3; 5];
L’array chiamato a conterrà 5 elementi che saranno tutti impostati sul valore 3 inizialmente. Questo è lo stesso che scrivere let a = [3, 3, 3, 3, 3];
ma in modo più conciso.
Accesso agli Elementi dell’Array
Un array è un unico blocco di memoria di una dimensione fissa conosciuta che può essere allocato nello stack. Puoi accedere agli elementi di un array usando l’indicizzazione, così:
Nome file: src/main.rs
rust
fn main() {
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
}
In questo esempio, la variabile denominata first otterrà il valore 1 perché è il valore all’indice [0] nell’array. La variabile denominata second otterrà il valore 2 dall’indice [1] nell’array.
Accesso a Elementi non Validi dell’Array
Vediamo cosa succede se provi ad accedere a un elemento di un array che è oltre la fine dell’array. Diciamo che esegui questo codice, simile al gioco dell’indovinello nel Capitolo 2, per accedere al sesto elemento di un array con cinque elementi:
Nome file: src/main.rs
rust
fn main() {
let a = [1, 2, 3, 4, 5]; let index = 10; let element = a[index];
println!("Il valore dell'elemento è: {element}");
}
Rust non consente l’accesso ad elementi fuori dagli array, e verrà visualizzato un errore:
less
$ cargo run
Compilazione di arrays v0.1.0 (file:///projects/arrays)
errore[E0277]: l'indice `10` è fuori dall'intervallo degli array di dimensione `5`
--> src/main.rs:5:17
|
5 | let element = a[index];
| ^ fuori dall'intervallo
|
= aiuto: gli indici devono essere positivi interi
= aiuto: forse vuoi accedere all'elemento `10` del slice `&a[..]`
Rust controlla che l’indice sia all’interno del range dell’array, e se non lo è, Rust visualizza un messaggio di errore e termina l’esecuzione del programma. Questo è un vantaggio rispetto ai linguaggi che consentono l’accesso agli elementi dell’array senza controllare se l’indice è all’interno del range dell’array, il che può causare comportamenti imprevedibili come la sovrascrittura di dati o l’accesso a dati non validi.
Puoi provare ad eseguire il codice e vedere il messaggio di errore che Rust visualizza quando prova ad accedere a un elemento dell’array che non esiste. Rimuoveremo poi il codice che causa l’errore e proseguiremo con il nostro esame degli array.