Variabili e tipi
Immutabilità
Come abbiamo visto, le variabili in Rust sono immutabili per default, questo potrebbe sembrare un ostacolo ma è una delle feature del linguaggio che tendono verso la safety e la sicurezza. Come abbiamo visto l'assegnamento di una variabile immutabile causa un errore di compilazione
fn main() { let var = 7; println!("var: {var}"); var = 4; println!("var: {var}"); }
provando a compilare, ci viene restituito questo errore:
error[E0384]: cannot assign twice to immutable variable `var`
--> src/main.rs:4:5
|
2 | let var = 7;
| ---
| |
| first assignment to `var`
| help: consider making this binding mutable: `mut var`
3 | println!("var: {var}");
4 | var = 4;
| ^^^^^^^ cannot assign twice to immutable variable
For more information about this error, try `rustc --explain E0384`.
Shadowing
Quello che però possiamo fare è il cosiddetto shadowing, ovvero possiamo definire un'altra variabile con lo stesso nome.
fn main() { let var = 7; println!("var: {var}"); let var = 4; println!("var: {var}"); }
Questa volta il programma compila con successo, e ci viene restituito l'output atteso
var: 7
var: 4
In realtà quello che stiamo facendo è mascherare la prima variabile con la seconda definizione, ma concettualmente sono variabili diverse, entrambe immutabili. Il compilatore Rust è abbastanza intelligente nella maggior parte dei casi e riesce a ottimizzare in fase di compilazione, non allocando ulteriori variabili sullo stack. Le variabili possono anche avere lo stesso nome ma tipi diversi
fn main() { let var = 7; println!("var: {var}"); let var = "stringa di test"; println!("var: {var}"); }
questo avviene perché effettivamente sono variabili diverse, e la prima variabile esce dall'ambiente al momento della seconda definizione.
Variabili mutabili
Possiamo comunque rendere le nostre variabili mutabili con la keyword mut, e se ne potrà cambiare il valore.
fn main() { let mut var = 7; println!("var: {var}"); var = 4; println!("var: {var}"); }
Come sopra, il risultato del programma è quello atteso:
var: 7
var: 4
Tipi di dato primitivi
Fino ad adesso non abbiamo specificato nessun tipo alle variabili, ma abbiamo detto all'inizio che Rust è un linguaggio di programmazione staticamente tipizzato.
Questo vuol dire che il tipo di ogni variabile deve essere definito a tempo di compilazione, e non può cambiare durante l'esecuzione del programma.
Come molti altri linguaggi, Rust supporta la type inference, ovvero per la maggior parte dei casi riesce a inferire il tipo delle variabili dal contesto.
Questa feature è simile alle funzionalità var di Java, e auto di c++.
Avremmo potuto annotare il tipo esplicitamente con la seguente notazione
#![allow(unused)] fn main() { let var: u32 = 7; }
dove u32 è il tipo di dato intero senza segno (unsigned) rappresentabile in 32 bit.
Rust supporta numerosi tipi di dato numerici, sia interi che a virgola mobile.
La lista completa è presente sulla reference ufficiale.
Su questi tipi di dato si possono effettuare le operazioni matematiche comuni
fn main() { let somma = 7 + 3; let sottrazione = 37.0 - 23.5; let prodotto = 17*23; let divisione = 50.3 / 23.1; let resto = 14 % 5; // => 4 let divisione_intera = 7 / 3; // => 2 }
Bisogna notare che le operazioni con gli interi possono risultare in overflow. Rust gestisce questa occorrenza in due casi:
- se il programma è compilato in modalità debug, include dei controlli e il programma si interrompe se incorre in un overflow
- se il programma è compilato in modalità release, non include questi controlli e al risultato applicato un wrap around.
Per un comportamento consistente, è possibile usare le funzioni che eseguono sempre il wrap come
wrapping_add, oppure usare le funzioni che eseguono sempre i controlli comechecked_add.
Altri tipi primitivi sono i booleani e i caratteri.
fn main() { let vero: bool = true; let falso: bool = false; // operatori logici let and = vero && falso; let or = vero || falso; let not = !vero; let c: char = 'A'; // è supportato anche utf-8 let fire: char = '🔥'; }
Tuple
Le tuple sono un modo per raggruppare elementi di tipo potenzialmente diverso in una unica struttura con dimensione fissa. Possiamo accedere agli elementi usando il loro indice, oppure tramite la destrutturazione. Vedremo che quest'ultimo è un caso particolare di una feature di Rust molto usata: il pattern matching.
Il programma
fn main() { let tupla: (i32, f64, bool) = (500, 6.4, true); println!("tupla: {:?}", tupla); // accesso con gli indici println!("tupla.0: {}", tupla.0); println!("tupla.1: {}", tupla.1); println!("tupla.2: {}", tupla.2); // destrutturazione let (x, y, z) = tupla; println!("x: {}", x); println!("y: {}", y); println!("z: {}", z); }
restituisce il seguente risultato.
tupla: (500, 6.4, true)
tupla.0: 500
tupla.1: 6.4
tupla.2: true
x: 500
y: 6.4
z: true
Array
Un array è una collezione di elementi con lo stesso tipo e con una lunghezza fissa. Si può dichiarare con gli elementi tra parentesi quadre, separati con una virgola.
fn main() { let a: [i32; 6] = [1, 34, 27, 2, -1, 0]; }
Il tipo [i32; 6] indica che l'array contiene sei elementi di tipo i32.
Array locali sono allocati sullo stack (in maniera similare agli array in c++).
Vedremo più avanti come creare collezioni memorizzate sullo heap con dimensione dinamica tramite la struttura Vec.
Si può accedere agli elementi con il loro indice, e Rust cerca di limitare gli accessi out of bound. Il seguente codice
fn main() { let a: [i32; 6] = [1, 34, 27, 2, -1, 0]; println!("a: {:?}", a); println!("a[0]: {:?}", a[0]); println!("a[3]: {:?}", a[3]); // indice out of bounds println!("a[6]: {:?}", a[6]); }
restituisce il seguente errore
error: this operation will panic at runtime
--> src/main.rs:9:28
|
9 | println!("a[6]: {:?}", a[6]);
| ^^^^ index out of bounds: the length is 6 but the index is 6
|
= note: `#[deny(unconditional_panic)]` on by default
Questo codice invece
fn main() { let a: [i32; 6] = [1, 34, 27, 2, -1, 0]; println!("a: {:?}", a); println!("a[0]: {:?}", a[0]); println!("a[3]: {:?}", a[3]); }
restituisce il seguente output
a: [1, 34, 27, 2, -1, 0]
a[0]: 1
a[1]: 2
Quando il compilatore non riesce a inferire la correttezza degli accessi, comunque inserisce dei controlli a runtime, e lancia una eccezione quando si prova ad accedere a un array out of bounds. Questa è una delle feature di memory safety di Rust, linguaggi come c o c++ semplicemente effettuano l'accesso anche se l'indice non è nei limiti dell'array.