Tipi di Dati
Ogni valore in Rust è di un certo tipo di dato, il quale indica a Rust che tipo di dato viene specificato in modo che sappia come lavorare con esso. Esamineremo due sottoinsiemi di tipi di dato: scalari e composti.
Tieni presente che Rust è un linguaggio a tipizzazione statica, il che significa che
deve conoscere i tipi di tutte le variabili al momento della compilazione. Il compilatore
può solitamente dedurre quale tipo vogliamo usare basandosi sul valore e su come lo
usiamo. Nei casi in cui sono possibili molti tipi, come quando abbiamo convertito una
String
a un tipo numerico usando parse
nella sezione “Confrontare il Guess con il Numero Segreto” nel Capitolo 2, dobbiamo aggiungere un'annotazione di tipo, come segue:
#![allow(unused)] fn main() { let guess: u32 = "42".parse().expect("Not a number!"); }
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:
$ cargo build
Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0282]: type annotations needed
--> src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Not a number!");
| ^^^^^
|
help: consider giving `guess` an explicit type
|
2 | let guess: _ = "42".parse().expect("Not a number!");
| +++
For more information about this error, try `rustc --explain E0282`.
error: could not compile `no_type_annotations` due to previous error
Vedrai annotazioni di tipo differenti per altri tipi di dati.
Tipi Scalari
Un tipo scalare rappresenta un singolo valore. Rust ha quattro tipi scalari principali: interi, numeri in virgola mobile, booleani e caratteri. Probabilmente li riconosci da altri linguaggi di programmazione. Scopriamo come funzionano in Rust.
Tipi Interi
Un intero è un numero senza 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 interi firmati iniziano con i
anziché u
)
che occupa 32 bit di spazio. La Tabella 3-1 mostra i tipi interi predefiniti in Rust.
Possiamo usare una qualsiasi di queste varianti per dichiarare il tipo di un valore intero.
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 firmata o 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 ha bisogno di avere un segno con esso (firmato) o se sarà sempre positivo e può quindi essere rappresentato senza un segno (non firmato). È come scrivere numeri su carta: quando il segno è importante, un numero è mostrato con un segno più o un segno meno; tuttavia, quando è sicuro assumere che il numero sia positivo, è mostrato senza segni. I numeri segno sono memorizzati utilizzando la rappresentazione in complemento a due.
Ogni variante segno può memorizzare numeri da -(2n - 1) a 2n -
1 - 1 inclusi, dove n è il numero di bit che la variante usa. Quindi un
i8
può memorizzare numeri da -(27) a 27 - 1, che equivale
a -128 a 127. Le varianti non firmate possono memorizzare numeri da 0 a 2n - 1, quindi un u8
può memorizzare numeri da 0 a 28 - 1,
che equivale a 0 a 255.
Inoltre, i tipi isize
e usize
dipendono dall'architettura del computer su
cui il tuo programma viene eseguito, che è denotato 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 numeri interi letterali in una qualsiasi delle forme mostrate
nella Tabella 3-2. Nota che i numeri letterali che possono essere di più tipi numerici
consentono un suffisso di tipo, come 57u8
, per designare il tipo. I numeri letterali
possono anche usare _
come separatore visivo per rendere il numero più facile da leggere, come 1_000
, che avrà lo stesso valore di se avessi specificato 1000
.
Numeri letterali | Esempio |
---|---|
Decimale | 98_222 |
Esadecimale | 0xff |
Ottale | 0o77 |
Binario | 0b1111_0000 |
Byte (u8 solo) | b'A' |
Quindi come sai 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 principale situazione in cui useresti isize
o
usize
è quando vengono indicizzati tipi di collezioni.
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 in un valore al di fuori di quel range, come 256, si verificherà un overflow intero, che può risultare in uno di due comportamenti. Quando stai compilando in modalità debug, Rust include controlli per l'overflow intero che causano il panic del programma a runtime se si verifica questo comportamento. Rust usa il termine panicking quando un programma termina con un errore; discuteremo i panic in modo più approfondito nella sezione “Errori Irrecuperabili conpanic!
” nel Capitolo 9.Quando stai compilando in modalità release con il flag
--release
, Rust non include controlli per l'overflow intero che causano panic. Invece, se si verifica un overflow, Rust esegue un Wrappando complemento a due. In breve, valori maggiori rispetto al valore massimo che il tipo può contenere si “wrappano” al minimo dei valori che il tipo può contenere. Nel caso di unu8
, 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 aspettavamo. Fare affidamento sul comportamento del Wrappando degli overflow interi è considerato un errore.Per gestire esplicitamente la possibilità di overflow, puoi utilizzare le famiglie di metodi fornite dalla libreria standard per i tipi numerici primitivi:
- Wrappa in tutte le modalità con i metodi
wrapping_*
, comewrapping_add
.- Restituisci il valore
None
se c'è overflow con i metodichecked_*
.- Restituisci il valore e un booleano che indica se c'è stato overflow con i metodi
overflowing_*
.- Saturati ai valori minimi o massimi 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. Il tipo predefinito è f64
perché
sui moderni CPU, è grosso modo della stessa velocità di f32
ma è capace di
più precisione. Tutti i tipi in virgola mobile sono firmati.
Ecco un esempio che mostra numeri in virgola mobile in azione:
Nome del 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. Il
tipo f32
è un float a singola precisione, e f64
ha precisione doppia.
Operazioni Numeriche
Rust supporta le operazioni matematiche di base che ti aspetteresti per tutti i
tipi numerici: addizione, sottrazione, moltiplicazione, divisione e resto. La
divisione intera tronca verso zero al numero intero più vicino. Il seguente
codice mostra come useresti ciascuna operazione numerica in un'istruzione
let
:
Nome del file: src/main.rs
fn main() { // addition let sum = 5 + 10; // subtraction let difference = 95.5 - 4.3; // multiplication let product = 4 * 30; // division let quotient = 56.7 / 32.2; let truncated = -5 / 3; // Results in -1 // remainder let remainder = 43 % 5; }
Ogni espressione in queste istruzioni utilizza un operatore matematico e risolve in un singolo valore, che viene quindi assegnato 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 sono di
un byte. Il tipo booleano in Rust è specificato usando bool
. Per esempio:
Nome del file: src/main.rs
fn main() { let t = true; let f: bool = false; // with explicit type annotation }
Il modo principale di usare i valori booleani è attraverso i costrutti
condizionali, come un'espressione if
. Discuteremo come funzionano le
espressioni if
in Rust nella sezione “Flusso di Controllo”.
Il Tipo Carattere
Il tipo char
di Rust è il tipo alfabetico più primitivo del linguaggio. Ecco
alcuni esempi di dichiarazione di valori char
:
Nome del file: src/main.rs
fn main() { let c = 'z'; let z: char = 'ℤ'; // with explicit type annotation let heart_eyed_cat = '😻'; }
Nota che specifichiamo i letterali char
con virgolette singole, al contrario
dei letterali di stringa, che usano virgolette doppie. Il tipo char
di Rust è
di quattro byte e rappresenta un Valore Scalare Unicode, il che significa che
può rappresentare molto più di semplici codici ASCII. Lettere accentate;
caratteri cinesi, giapponesi e coreani; emoji; e spazi di larghezza zero sono
tutti valori char
validi in Rust. I Valori Scalare Unicode 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 di cosa sia un
“carattere” potrebbe non corrispondere a ciò che è un char
in Rust.
Discuteremo questo argomento in dettaglio in “Memorizzare Testo Codificato in
UTF-8 con le Stringhe” nel Capitolo 8.
Tipi Composti
I tipi composti possono raggruppare più valori in un unico tipo. Rust ha due tipi composti primitivi: tuple e array.
Il Tipo Tuple
Una tupla è un modo generale di raggruppare un certo numero di valori con una varietà di tipi in un solo tipo composto. Le tuple hanno una lunghezza fissa: una volta dichiarate, non possono crescere o ridursi in dimensione.
Creiamo una tupla scrivendo un elenco di valori separati da virgole tra parentesi. Ogni posizione nella tupla ha un tipo, e i tipi dei diversi valori nella tupla non devono essere uguali. Abbiamo aggiunto annotazioni di tipo opzionali in questo esempio:
Nome del file: src/main.rs
fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); }
La variabile tup
associa all'intera tupla perché una tupla è considerata un
singolo elemento composto. Per ottenere i singoli valori da una tupla,
possiamo usare il pattern matching per destrutturare un valore tupla, come
questo:
Nome del file: src/main.rs
fn main() { let tup = (500, 6.4, 1); let (x, y, z) = tup; println!("The value of y is: {y}"); }
Questo programma prima crea una tupla e la associa alla variabile tup
. Poi
usa un pattern con let
per prendere tup
e trasformarlo in tre variabili
separate, x
, y
, e z
. Questo è chiamato destrutturare perché divide 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. Per esempio:
Nome del file: src/main.rs
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 poi accede a ciascun elemento della tupla
usando i loro rispettivi indici. Come nella 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
corrispondente tipo sono entrambi scritti ()
e rappresentano un valore
vuoto o un tipo di ritorno vuoto. Le espressioni restituiscono implicitamente
il valore unità se non restituiscono alcun altro valore.
Il Tipo Array
Un altro modo per avere una collezione di più valori è 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 un elenco separato da virgole tra parentesi quadre:
Nome del file: src/main.rs
fn main() { let a = [1, 2, 3, 4, 5]; }
Gli array sono utili quando si desidera che i tuoi dati siano allocati nello stack piuttosto che nell'heap (discuteremo maggiormente lo stack e l'heap nel Capitolo 4) o quando vuoi garantire di avere sempre un numero fisso di elementi. Tuttavia, un array non è flessibile come il tipo vettoriale. Un vettore è un tipo di collezione simile fornito dalla libreria standard che è consentito crescere o ridursi in dimensione. Se non sei sicuro se usare un array o un vettore, è probabile che dovresti usare un vettore. Il Capitolo 8 discute i vettori in maggiore dettaglio.
Tuttavia, gli array sono più utili quando sai che il numero di elementi non cambierà. Per esempio, se stai usando i nomi dei mesi in un programma, useresti probabilmente un array piuttosto che un vettore perché sai che conterrà sempre 12 elementi:
#![allow(unused)] fn main() { let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; }
Scrivi il tipo di un array usando parentesi quadre con il tipo di ciascun elemento, un punto e virgola, e poi il numero di elementi nell'array, in questo modo:
#![allow(unused)] fn main() { let a: [i32; 5] = [1, 2, 3, 4, 5]; }
Qui, i32
è il tipo di ciascun 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 ciascun 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 inizialmente tutti
impostati al valore 3
. Questo è lo stesso che scrivere let a = [3, 3, 3, 3, 3];
ma in un modo più conciso.
Accedere agli Elementi di un Array
Un array è un blocco unico di memoria di dimensione nota e fissa che può essere allocato nello stack. Puoi accedere agli elementi di un array usando l'indicizzazione, come questo:
Nome del file: src/main.rs
fn main() { let a = [1, 2, 3, 4, 5]; let first = a[0]; let second = a[1]; }
In questo esempio, la variabile chiamata first
otterrà il valore 1
perché
questo è il valore all'indice [0]
nell'array. La variabile chiamata second
otterrà il valore 2
dall'indice [1]
nell'array.
Accesso non Valido all'Elemento di un Array
Vediamo cosa succede se provi ad accedere a un elemento di un array che è oltre la fine dell'array. Supponiamo di eseguire questo codice, simile al gioco Guessing del Capitolo 2, per ottenere un indice di array dall'utente:
Nome del file: src/main.rs
use std::io;
fn main() {
let a = [1, 2, 3, 4, 5];
println!("Please enter an array index.");
let mut index = String::new();
io::stdin()
.read_line(&mut index)
.expect("Failed to read line");
let index: usize = index
.trim()
.parse()
.expect("Index entered was not a number");
let element = a[index];
println!("The value of the element at index {index} is: {element}");
}
Questo codice si compila con successo. 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 10
, vedrai un output simile a questo:
thread 'main' panicked at src/main.rs:19:19:
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 runtime al momento dell'utilizzo di un valore non valido nell'operazione di indicizzazione. Il programma è terminato con un messaggio di errore e non ha eseguito l'istruzione finale println!
. Quando tenti di accedere a un elemento utilizzando l'indicizzazione, Rust controllerà che l'indice specificato sia minore della lunghezza dell'array. Se l'indice è maggiore o uguale alla lunghezza, Rust andrà in panic. Questo controllo deve avvenire a runtime, specialmente in questo caso, perché il compilatore non può sapere a priori quale valore inserirà un utente quando eseguirà il codice più tardi.
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 eseguito, e quando fornisci un indice errato, è possibile accedere a memoria non valida. Rust ti protegge da questo tipo di errore terminando immediatamente l'esecuzione invece di consentire l'accesso alla memoria e continuare. Il Capitolo 9 discute più approfonditamente la gestione degli errori in Rust e come puoi scrivere codice leggibile e sicuro che non va in panic né consente l'accesso a memoria non valida.