SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI
Controllo del Flusso
La capacità di eseguire del codice in base alla verità di una condizione e di eseguire del codice ripetutamente mentre una condizione è vera sono elementi fondamentali nella maggior parte dei linguaggi di programmazione. I costrutti più comuni che ti permettono di controllare il flusso di esecuzione del codice Rust sono le espressioni if e i cicli. Espressioni if
Un’espressione if ti permette di ramificare il tuo codice in base a delle condizioni. Fornisci una condizione e quindi affermi: “Se questa condizione è soddisfatta, esegui questo blocco di codice. Se la condizione non è soddisfatta, non eseguire questo blocco di codice.”
Crea un nuovo progetto chiamato “branches” nella tua directory dei progetti per esplorare l’espressione if. Nel file src/main.rs, inserisci quanto segue:
Nome file: src/main.rs
rust
fn main() {
let number = 3;
if number < 5 {
println!("la condizione era vera");
} else {
println!("la condizione era falsa");
}
}
Tutte le espressioni if iniziano con la parola chiave if, seguita da una condizione. In questo caso, la condizione verifica se la variabile number ha un valore inferiore a 5. Posizioniamo il blocco di codice da eseguire se la condizione è vera immediatamente dopo la condizione tra parentesi graffe. I blocchi di codice associati alle condizioni nelle espressioni if vengono a volte chiamati bracci, proprio come i bracci nelle espressioni match di cui abbiamo parlato nella sezione “Confronto del Guess col Numero Segreto” del Capitolo 2.
Facoltativamente, possiamo anche includere un’espressione else, come abbiamo scelto di fare qui, per dare al programma un blocco di codice alternativo da eseguire nel caso in cui la condizione venga valutata come falsa. Se non fornisci un’espressione else e la condizione è falsa, il programma salterà semplicemente il blocco if e passerà al codice successivo.
Prova a eseguire questo codice; dovresti vedere l’output seguente:
bash
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
la condizione era vera
Proviamo a cambiare il valore di number in un valore che rende la condizione falsa per vedere cosa succede:
rust
let number = 7;
Esegui nuovamente il programma e guarda l’output:
bash
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
la condizione era falsa
È anche importante notare che la condizione in questo codice deve essere di tipo bool. Se la condizione non è di tipo bool, otterremo un errore. Ad esempio, prova a eseguire il seguente codice:
Nome file: src/main.rs
rust
fn main() {
let number = 3;
if number {
println!("number era tre");
}
}
La condizione if si valuta a un valore di 3 questa volta e Rust restituisce un errore:
bash
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: tipi non corrispondenti
--> src/main.rs:4:8
|
4 | if number {
| ^^^^^^ previsto `bool`, trovato intero
Per ulteriori informazioni su questo errore, prova `rustc --explain E0308`.
error: non è stato possibile compilare `branches` a causa di un errore precedente
L’errore indica che Rust si aspettava un bool ma ha trovato un intero. A differenza di linguaggi come Ruby e JavaScript, Rust non cercherà automaticamente di convertire tipi non booleani in un booleano. Devi essere esplicito e fornire sempre a if un booleano come sua condizione. Se vogliamo che il blocco di codice if venga eseguito solo quando un numero non è uguale a 0, ad esempio, possiamo cambiare l’espressione if nel seguente modo:
Nome file: src/main.rs
rust
fn main() {
let number = 3;
if number != 0 {
println!("number era diverso da zero");
}
}
Eseguire questo codice stamperà “number era diverso da zero”. Gestione di Più Condizioni con else if
Puoi utilizzare condizioni multiple combinando if ed else in un’espressione else if. Ad esempio:
Nome file: src/main.rs
rust
fn main() {
let number = 6;
if number % 4 == 0 {
println!("il numero è divisibile per 4");
} else if number % 3 == 0 {
println!("il numero è divisibile per 3");
} else if number % 2 == 0 {
println!("il numero è divisibile per 2");
} else {
println!("il numero non è divisibile per 4, 3 o 2");
}
}
Questo programma ha quattro percorsi possibili da intraprendere. Dopo averlo eseguito, dovresti vedere il seguente output:
bash
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/branches`
il numero è divisibile per 3
Quando questo programma viene eseguito, verifica ogni espressione if in sequenza ed esegue il primo blocco per cui la condizione si valuta come vera. Nota che anche se 6 è divisibile per 2, non vediamo l’output “il numero è divisibile per 2”, né vediamo il testo “il numero non è divisibile per 4, 3 o 2” dal blocco else. Questo perché Rust esegue solo il blocco per la prima condizione vera e, una volta trovata una, non controlla nemmeno il resto. L’utilizzo di troppe espressioni else if può ingombrare il codice, quindi se ne hai più di una, potresti voler ristrutturare il tuo codice. Il Capitolo 6 descrive un potente costrutto di branching di Rust chiamato match per questi casi. Utilizzo di if in un’espressione let
Poiché if è un’espressione, possiamo usarlo sul lato destro di una dichiarazione let per assegnare l’output a una variabile, come in Listing 3-2.
Nome file: src/main.rs
rust
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("Il valore di number è: {number}");
}
Il numero variabile sarà legato a un valore basato sull’esito dell’espressione if. Esegui questo codice per vedere cosa succede:
bash
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/branches`
Il valore di number è: 5
Ricorda che i blocchi di codice si valutano all’ultima espressione in essi, e i numeri per conto proprio sono anche espressioni. In questo caso, il valore dell’intera espressione if dipende dal blocco di codice che si esegue. Ciò significa che i valori che hanno il potenziale per essere risultati da ogni braccio dell’if devono essere dello stesso tipo; in Listing 3-2, i risultati sia del braccio if che del braccio else erano interi i32. Se i tipi non corrispondono, come nell’esempio seguente, otterremo un errore:
Nome file: src/main.rs
rust
fn main() {
let condition = true; let number = if condition { 5 } else { "sei" };
println!("Il valore di number è: {number}");
}
Quando proviamo a compilare questo codice, otterremo un errore. I bracci if ed else hanno tipi di valore incompatibili, e Rust indica esattamente dove trovare il problema nel programma:
bash
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` e `else` hanno tipi incompatibili
--> src/main.rs:4:44
|
4 | let number = if condition { 5 } else { "sei" };
| - ^^^^^ previsto intero, trovato `&str`
| |
| previsto a causa di questo
Per ulteriori informazioni su questo errore, prova `rustc --explain E0308`.
error: non è stato possibile compilare `branches` a causa di un errore precedente
L’espressione nel blocco if si valuta a un intero, e l’espressione nel blocco else si valuta a una stringa. Questo non funzionerà perché le variabili devono avere un singolo tipo, e Rust deve sapere a tempo di compilazione quale tipo sia definitivamente la variabile number. Conoscere il tipo di number consente al compilatore di verificare che il tipo sia valido ovunque si utilizzi number. Rust non sarebbe in grado di farlo se il tipo di number venisse determinato solo a tempo di esecuzione; il compilatore sarebbe più complesso e farebbe meno garanzie sul codice se dovesse tenere traccia di più tipi ipotetici per qualsiasi variabile. Ripetizione con Cicli
È spesso utile eseguire un blocco di codice più di una volta. Per questa operazione, Rust fornisce diversi cicli, che eseguiranno il codice all’interno del corpo del ciclo fino alla fine e quindi ricominceranno immediatamente dall’inizio. Per sperimentare con i cicli, creiamo un nuovo progetto chiamato “loops”.
Rust ha tre tipi di cicli: loop, while e for. Proviamo ognuno. Ripetizione del Codice con loop
La parola chiave loop indica a Rust di eseguire un blocco di codice più e più volte per sempre o fino a quando non gli dici esplicitamente di smettere.
Come esempio, cambia il file src/main.rs nella tua directory loops in questo modo:
Nome file: src/main.rs
rust
fn main() {
loop {
println!("di nuovo!");
}
}
Quando eseguiamo questo programma, vedremo di nuovo! stampato continuamente finché non interrompiamo manualmente il programma. La maggior parte dei terminali supporta la scorciatoia da tastiera ctrl-c per interrompere un programma bloccato in un loop continuo. Prova:
bash
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.29s
Running `target/debug/loops`
di nuovo!
di nuovo!
di nuovo!
di nuovo!
^Cdi nuovo!
Il simbolo ^C rappresenta dove hai premuto ctrl-c. Potresti o meno vedere la parola di nuovo! stampata dopo il ^C, a seconda di dove si trovava il codice nel loop quando ha ricevuto il segnale di interruzione.
Fortunatamente, Rust fornisce anche un modo per uscire da un ciclo usando il codice. Puoi inserire la parola chiave break all’interno del ciclo per dire al programma quando smettere di eseguire il loop. Ricorda che abbiamo fatto questo nel gioco di indovinello nella sezione “Uscita Dopo un Indovinello Corretto” del Capitolo 2 per uscire dal programma quando l’utente ha vinto il gioco indovinando il numero corretto.
Abbiamo anche usato continue nel gioco di indovinello, che in un ciclo dice al programma di saltare il resto del codice in questa iterazione del ciclo e passare all’iterazione successiva. Ritorno di Valori da Cicli
Uno degli utilizzi di un ciclo è riprovare un’operazione che potrebbe fallire, ad esempio controllare se un thread ha completato il suo lavoro. Potresti anche avere bisogno di passare il risultato di quell’operazione fuori dal ciclo al resto del tuo codice. Per fare ciò, puoi aggiungere il valore che desideri restituire dopo l’espressione break che usi per interrompere il ciclo; quel valore verrà restituito fuori dal ciclo in modo che tu possa usarlo, come mostrato qui:
rust
fn main() {
let mut counter = 0; let result = loop {
counter += 1; if counter == 10 {
break counter * 2;
}
};
println!("Il risultato è {result}");
}
Prima del ciclo, dichiariamo una variabile chiamata counter e la inizializziamo a 0. Quindi dichiariamo una variabile chiamata result per contenere il valore restituito dal ciclo. Ad ogni iterazione del ciclo, aggiungiamo 1 alla variabile counter e quindi controlliamo se il contatore è uguale a 10. Quando lo è, usiamo la parola chiave break con il valore counter * 2. Dopo il ciclo, utilizziamo un punto e virgola per terminare l’istruzione che assegna il valore a result. Infine, stampiamo il valore in result, che in questo caso è 20. Etichette di Ciclo per Dissociare Tra Più Cicli
Se hai cicli annidati, break e continue si applicano al ciclo più interno in quel punto. Puoi specificare facoltativamente un’etichetta di ciclo su un ciclo che poi puoi utilizzare con break o continue per specificare che quelle parole chiave si applicano al ciclo etichettato invece che al ciclo più interno. Le etichette di ciclo devono iniziare con un singolo apice. Ecco un esempio con due cicli annidati:
rust
fn main() {
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10; loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up;
}
remaining -= 1;
}
count += 1;
}
println!("End count = {count}");
}
Il ciclo esterno ha l’etichetta ‘counting_up e conta da 0 a 2. Il ciclo interno senza etichetta conta da 10 a 9. Il primo break che non specifica un’etichetta uscirà solo dal ciclo interno. L’istruzione break ‘counting_up; uscirà dal ciclo esterno. Questo codice stampa:
bash
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.58s
Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2
Cicli Condizionali con while
Un programma spesso deve valutare una condizione all’interno di un ciclo. Finché la condizione è vera, il ciclo viene eseguito. Quando la condizione smette di essere vera, il programma chiama break, fermando il ciclo. È possibile implementare comportamenti come questo utilizzando una combinazione di loop, if, else e break; potresti provarlo ora in un programma, se vuoi. Tuttavia, questo modello è così comune che Rust ha una costruzione del linguaggio integrata per questo, chiamata un ciclo while. In Listing 3-3, usiamo while per eseguire il programma tre volte, contando ogni volta, e quindi, dopo il ciclo, stampiamo un messaggio ed usciamo.
Nome file: src/main.rs
rust
fn main() {
let mut number = 3; while number != 0 {
println!("{number}!"); number -= 1;
}
println!("LIFTOFF!!!");
}
Questo costrutto elimina molti annidamenti che sarebbero necessari se si utilizzassero loop, if, else e break, ed è più chiaro. Finché una condizione si valuta come vera, il codice viene eseguito; altrimenti, esce dal ciclo. Ciclo Attraverso una Collezione con for
Puoi scegliere di utilizzare la costruzione while per iterare sugli elementi di una collezione, come un array. Ad esempio, il ciclo in Listing 3-4 stampa ogni elemento nell’array a.
Nome file: src/main.rs
rust
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
println!("il valore è: {a[index]}");
index += 1;
}
}
Poiché questa è una struttura comune, Rust fornisce un’altra costruzione chiamata for per lavorare con gli array (e altre collezioni) in modo più conciso e intuitivo. Il codice diventa più semplice se utilizzi for per attraversare la collezione:
Nome file: src/main.rs
rust
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("il valore è: {element}");
}
}
In questo codice, per ogni iterazione, un valore da a viene assegnato alla variabile element e il corpo del ciclo viene eseguito. Il metodo iter restituisce ogni elemento in turno, e il ciclo li stampa. L’output è lo stesso di prima:
bash
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/loops`
il valore è: 10
il valore è: 20
il valore è: 30
il valore è: 40
il valore è: 50
for è anche utile per eseguire un codice un certo numero di volte. Ad esempio, eseguire il codice in Listing 3-6 scriverebbe cinque numeri crescenti in ordine.
Nome file: src/main.rs
rust
fn main() {
for number in (1..4).rev() {
println!("{number}!");
}
println!("LIFTOFF!!!");
}
L’espressione (1..4) genera una serie di numeri da 1 a 3, inclusi. Questo è un intervallo. Il .rev chiama il metodo rev su questo intervallo per contare all’indietro. Anche se contiamo fino a 3, utilizziamo .rev per contare dal numero più grande al più piccolo. L’output di questo codice è:
bash
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.29s
Running `target/debug/loops`
3!
2!
1!
LIFTOFF!!!
Questo esempio mostra anche come, quando utilizzi for, puoi specificare un intervallo di numeri. Abbiamo già visto questo approccio nel Capitolo 2 nella sezione “Ripetizione con Range”. Invece di avere un elenco di elementi da cui estrarre, puoi specificare un intervallo e for eseguirà il corpo del ciclo per ogni numero nell’intervallo. Il .rev chiama il metodo rev su quell’intervallo per contare all’indietro invece che in avanti. Questo è molto utile per contare all’indietro da un numero più grande a uno più piccolo.