Controllo del flusso
La possibilità di eseguire del codice a seconda che una condizione sia vera
e
di eseguire ripetutamente del codice finché una data condizione è vera
sono
elementi fondamentali della maggior parte dei linguaggi di programmazione. I
costrutti più comuni che ti permettono di controllare il flusso di esecuzione
del codice in Rust sono le espressioni if
e i cicli.
L’espressione if
Un’espressione if
(se
in italiano) ti permette di ramificare il tuo codice a
seconda delle condizioni. Fornisci una condizione e poi dici: “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 ramificazioni nella tua directory progetti
per sperimentare con l’espressione if
. Nel file src/main.rs, inserisci
quanto segue:
File: src/main.rs
fn main() { let numero = 3; if numero < 5 { println!("condizione era vera"); } else { println!("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 numero
ha o
meno un valore inferiore a 5. Il blocco di codice da eseguire se la condizione è
true
viene posizionato subito dopo la condizione, all’interno di parentesi
graffe. I blocchi di codice associati alle condizioni nelle espressioni if
possono esser viste come dei rami, proprio come i rami nelle espressioni
match
di cui abbiamo parlato nella sezione “Confrontare l’ipotesi con il
numero segreto” del
Capitolo 2.
Opzionalmente, possiamo anche includere un’espressione else
(altrimenti
in
italiano), come abbiamo scelto di fare in questo caso, per dare al programma un
blocco di codice alternativo da eseguire nel caso in cui la condizione sia
valutata false
. Se non fornisci un’espressione else
e la condizione è
false
, il programma salterà il blocco if
e passerà alla parte di codice
successiva.
Prova a eseguire questo codice; dovresti vedere il seguente risultato:
$ cargo run
Compiling ramificazioni v0.1.0 (file:///progetti/ramificazioni)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.06s
Running `target/debug/ramificazioni`
condizione era vera
Proviamo a cambiare il valore di numero
con un valore che renda la condizione
false
per vedere cosa succede:
fn main() {
let numero = 7;
if numero < 5 {
println!("condizione era vera");
} else {
println!("condizione era falsa");
}
}
Esegui nuovamente il programma e guarda l’output:
$ cargo run
Compiling ramificazioni v0.1.0 (file:///progetti/ramificazioni)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
Running `target/debug/ramificazioni`
condizione era falsa
Vale anche la pena di notare che la condizione in questo codice deve essere un
bool
. Se la condizione non è un bool
, otterremo un errore. Ad esempio, prova
a eseguire il seguente codice:
File: src/main.rs
fn main() {
let numero = 3;
if numero {
println!("numero era tre");
}
}
Questa volta la condizione if
valuta un valore di 3
e Rust lancia un errore:
$ cargo run
Compiling ramificazioni v0.1.0 (file:///progetti/ramificazioni)
error[E0308]: mismatched types
--> src/main.rs:4:8
|
4 | if numero {
| ^^^^^^ expected `bool`, found integer
For more information about this error, try `rustc --explain E0308`.
error: could not compile `ramificazioni` (bin "ramificazioni") due to 1 previous error
L’errore indica che Rust si aspettava un bool
ma ha ottenuto un numero intero.
A differenza di linguaggi come Ruby e JavaScript, Rust non cercherà
automaticamente di convertire i type non booleani in booleani. Devi essere
esplicito e fornire sempre ad if
un’espressione booleana come condizione. Se
vogliamo che il blocco di codice if
venga eseguito solo quando un numero non è
uguale a 0
, ad esempio, possiamo modificare l’espressione if
nel seguente
modo:
File: src/main.rs
fn main() { let numero = 3; if numero != 0 { println!("numero era qualcosa di diverso da zero"); } }
L’esecuzione di questo codice stamperà numero era qualcosa di diverso da zero
.
Gestione di condizioni multiple con else if
Puoi utilizzare condizioni multiple combinando if
e else
in un’espressione
else if
. Ad esempio:
File: src/main.rs
fn main() { let numero = 6; if numero % 4 == 0 { println!("numero è divisibile per 4"); } else if numero % 3 == 0 { println!("numero è divisibile per 3"); } else if numero % 2 == 0 { println!("numero è divisibile per 2"); } else { println!("numero non è divisibile per by 4, 3, o 2"); } }
Questo programma ha quattro possibili rami. Dopo averlo eseguito, dovresti vedere il seguente output:
$ cargo run
Compiling ramificazioni v0.1.0 (file:///progetti/ramificazioni)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.36s
Running `target/debug/ramificazioni`
numero è divisibile per 3
Quando questo programma viene eseguito, controlla ogni espressione if
a turno
ed esegue il primo corpo per il quale la condizione è valutata true
. Nota che
anche se 6 è divisibile per 2, non vediamo l’output numero è divisibile per 2
,
né vediamo il testo numero non è divisibile per 4, 3 o 2
del blocco else
.
Questo perché Rust esegue il blocco solo per la prima condizione true
e una
volta che ne trova una, le restanti non vengono controllate.
L’uso di troppe espressioni else if
può rendere il codice un po’ confusionario
e difficile da leggere, quindi se ne hai più di una, potresti valutare di
riscrivere il codice. Il Capitolo 6 descrive un potente costrutto di
ramificazione di Rust chiamato match
per gestire casi del genere.
Utilizzo di if
in una dichiarazione let
Dato che if
è un’espressione, possiamo usarla a destra di una dichiarazione
let
per assegnare il risultato a una variabile, come nel Listato 3-2.
fn main() { let condizione = true; let numero = if condizione { 5 } else { 6 }; println!("Il valore di numero è: {numero}"); }
if
as una variabileLa variabile numero
sarà legata a un valore basato sul risultato
dell’espressione if
. Esegui questo codice per vedere cosa succede:
$ cargo run
Compiling ramificazioni v0.1.0 (file:///progetti/ramificazioni)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.40s
Running `target/debug/ramificazioni`
Il valore di numero è: 5
Ricorda che i blocchi di codice valutano l’ultima espressione in essi contenuta
e i numeri da soli sono anch’essi espressioni. In questo caso, il valore
dell’intera espressione if
dipende da quale blocco di codice viene eseguito.
Ciò significa che i valori che possono essere i risultati di ogni ramo di if
devono essere dello stesso tipo; nel Listato 3-2, i risultati sia del ramo
if
che del ramo else
erano numeri interi i32
. Se i type non sono
corrispondenti, come nell’esempio seguente, otterremo un errore:
File: src/main.rs
fn main() {
let condizione = true;
let numero = if condizione { 5 } else { "sei" };
println!("Il valore di numero è: {numero}");
}
Quando proviamo a compilare questo codice, otterremo un errore: i rami if
e
else
hanno type incompatibili e Rust indica esattamente dove trovare il
problema nel programma:
$ cargo run
Compiling ramificazioni v0.1.0 (file:///progetti/ramificazioni)
error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:4:45
|
4 | let numero = if condizione { 5 } else { "sei" };
| - ^^^^^ expected integer, found `&str`
| |
| expected because of this
For more information about this error, try `rustc --explain E0308`.
error: could not compile `ramificazioni` (bin "ramificazioni") due to 1 previous error
L’espressione nel blocco if
ritorna un integer e l’espressione nel blocco
else
ritorna una stringa. Questo non funziona perché le variabili devono avere
un type univoco e Rust ha bisogno di sapere in fase di compilazione di che
type è la variabile numero
, in modo definitivo. Conoscere il type di
numero
permette al compilatore di verificare che il type sia valido ovunque
si utilizzi numero
. Rust non sarebbe in grado di farlo se il type di
numero
fosse determinato solo in fase di esecuzione; il compilatore sarebbe
più complesso e darebbe meno garanzie sul codice se dovesse tenere traccia dei
più disparati type possibili per ogni variabile.
Ripetizione con i cicli
Spesso è utile eseguire un blocco di codice più di una volta. Per questo compito, Rust mette a disposizione diversi cicli (loop in inglese), che eseguono il codice all’interno del corpo del ciclo fino alla fine e poi ripartono immediatamente dall’inizio. Per sperimentare con i cicli, creiamo un nuovo progetto chiamato cicli.
Rust mette a disposizione tre tipologie di ciclo: loop
, while
e for
.
Proviamo ciascuno di essi.
Ripetizione del codice con loop
La parola chiave loop
dice a Rust di eseguire un blocco di codice più e più
volte per sempre o finché non gli dici esplicitamente di fermarsi.
A titolo di esempio, modifica il file src/main.rs nella tua cartella cicli in questo modo:
File: src/main.rs
fn main() {
loop {
println!("ancora!");
}
}
Quando eseguiamo questo programma, vedremo ancora!
stampato in continuazione
fino a quando non interromperemo il programma manualmente. La maggior parte dei
terminali supporta la scorciatoia da tastiera ctrl-c per
interrompere un programma che è bloccato in un ciclo continuo. Provaci:
$ cargo run
Compiling cicli v0.1.0 (file:///progetti/cicli)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/cicli`
ancora!
ancora!
ancora!
ancora!
^Ccicli!
Il simbolo ^C
rappresenta quando hai premuto ctrl-c.
Potresti vedere o meno la parola ancora!
stampata dopo la ^C
, a seconda di
dove si trovava il codice nel ciclo quando ha ricevuto il segnale di
interruzione.
Fortunatamente, Rust offre anche un modo per uscire da un ciclo utilizzando del
codice. Puoi inserire la parola chiave break
all’interno del ciclo per
indicare al programma quando interrompere l’esecuzione del ciclo. Ricorda che
abbiamo fatto questo nel gioco di indovinelli nella sezione “Uscita dopo
un’ipotesi corretta” del
Capitolo 2 per uscire dal programma quando l’utente indovinava il numero
segreto.
Nel gioco di indovinelli abbiamo usato anche continue
, che in un ciclo indica
al programma di saltare tutto il codice rimanente in questa iterazione del ciclo
e di passare all’iterazione successiva.
Restituzione di valori dai cicli
Uno degli utilizzi di un loop
è quello di riprovare un’operazione che sai che
potrebbe fallire, come ad esempio controllare se un thread ha completato il
suo lavoro. Potresti anche aver bisogno di passare il risultato di questa
operazione al di fuori del ciclo al resto del tuo codice. Per farlo, puoi
aggiungere il valore che vuoi che venga restituito dopo l’espressione break
che utilizzi per interrompere il ciclo; quel valore verrà restituito al di fuori
del ciclo in modo da poterlo utilizzare, come mostrato qui:
fn main() { let mut contatore = 0; let risultato = loop { contatore += 1; if contatore == 10 { break contatore * 2; } }; println!("Il risultato è {risultato}"); }
Prima del ciclo, dichiariamo una variabile chiamata contatore
e la
inizializziamo a 0
. Poi dichiariamo una variabile chiamata risultato
per
contenere il valore restituito dal ciclo. A ogni iterazione del ciclo,
aggiungiamo 1
alla variabile contatore
e poi controlliamo se contatore
è
uguale a 10
. Quando lo è, usiamo la parola chiave break
con il valore
contatore * 2
. Dopo il ciclo, usiamo un punto e virgola per terminare
l’istruzione che assegna il valore a risultato
. Infine, stampiamo il valore in
risultato
, che in questo caso è 20
.
Puoi anche usare return
all’interno di un ciclo. Mentre break
esce solo dal
ciclo corrente, return
esce sempre dalla funzione corrente.
Etichette di loop per distinguere tra cicli multipli
Se hai un ciclo annidati all’interno di un altro ciclo, break
e continue
si
applicano al loop più interno in quel momento. Puoi specificare facoltativamente
un’etichetta (loop label) su uno specifico ciclo per poi usare con break
o
continue
quell’etichetta per specificare a quale ciclo applicare l’istruzione.
Le loop label devono iniziare con una virgoletta singola. Ecco un esempio con
due cicli annidati:
fn main() { let mut conteggio = 0; 'aumenta_conteggio: loop { println!("conteggio = {conteggio}"); let mut rimanente = 10; loop { println!("rimanente = {rimanente}"); if rimanente == 9 { break; } if conteggio == 2 { break 'aumenta_conteggio; } rimanente -= 1; } conteggio += 1; } println!("Fine conteggio = {conteggio}"); }
Il ciclo esterno ha la label 'aumenta_conteggio
e conta da 0 a 2. Il ciclo
interno senza label conta da 10 a 9. Il primo break
che non specifica una
label esce solo dal ciclo interno. L’istruzione break 'aumenta_conteggio;
esce dal ciclo esterno. Questo codice stamperà:
$ cargo run
Compiling cicli v0.1.0 (file:///progetti/cicli)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.35s
Running `target/debug/cicli`
conteggio = 0
rimanente = 10
rimanente = 9
conteggio = 1
rimanente = 10
rimanente = 9
conteggio = 2
rimanente = 10
Fine conteggio = 2
I cicli condizionali con while
Spesso un programma ha bisogno di valutare una condizione all’interno di un
ciclo. Quando la condizione è true
, il ciclo viene eseguito. Quando la
condizione cessa di essere true
, il programma chiama break
, interrompendo il
ciclo. È possibile implementare un comportamento del genere utilizzando una
combinazione di loop
, if
, else
e break
; se vuoi, puoi provare a farlo in
un programma. Tuttavia, questo schema è così comune che Rust ha un costrutto di
linguaggio incorporato per questi casi, chiamato ciclo while
(finché in
italiano). Nel Listato 3-3, usiamo while
per eseguire il ciclo del programma
tre volte, contando alla rovescia ogni volta, e poi, dopo il ciclo, stampiamo un
messaggio e usciamo.
fn main() { let mut numero = 3; while numero != 0 { println!("{numero}!"); numero -= 1; } println!("PARTENZA!!!"); }
while
per eseguire codice finché la condizione è true
Questo costrutto elimina un sacco di annidamenti che sarebbero necessari se
usassi loop
, if
, else
e break
, ed è di più semplice lettura. Finchè una
condizione risulta true
, il codice viene eseguito; altrimenti, esce dal ciclo.
Eseguire un ciclo su una collezione con for
Puoi scegliere di utilizzare il costrutto while
per eseguire un ciclo sugli
elementi di una collezione, come un array. Ad esempio, il ciclo nel Listato 3-4
stampa ogni elemento dell’array a
.
fn main() { let a = [10, 20, 30, 40, 50]; let mut indice = 0; while indice < 5 { println!("il valore è: {}", a[indice]); indice += 1; } }
while
In questo caso, il codice conteggia tutti gli elementi dell’array: inizia
dall’indice 0
e poi esegue un ciclo fino a raggiungere l’ultimo indice
dell’array (cioè quando indice < 5
non è più true
). L’esecuzione di questo
codice stamperà ogni elemento dell’array:
$ cargo run
Compiling cicli v0.1.0 (file:///progetti/cicli)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.36s
Running `target/debug/cicli`
il valore è: 10
il valore è: 20
il valore è: 30
il valore è: 40
il valore è: 50
Tutti e cinque i valori dell’array appaiono nel terminale, come previsto.
Anche se indice
raggiungerà un valore di 5
a un certo punto, il ciclo viene
bloccato prima che si tenti di leggere un sesto elemento dell’array.
Tuttavia, questo approccio è incline all’errore; potremmo causare il panic del
programma se il valore dell’indice o la condizione di test non sono corretti.
Per esempio, se cambiassi la definizione dell’array a
per avere quattro
elementi, ma dimenticassi di aggiornare la condizione a while indice < 4
, il
codice andrebbe in panic. È anche lento, perché il compilatore aggiunge codice
di runtime per eseguire il controllo condizionale per verificare se l’indice è
entro i limiti dell’array a ogni iterazione del ciclo.
Come alternativa più concisa, puoi usare un ciclo for
ed eseguire del codice
per ogni elemento di una collezione. Un ciclo for
assomiglia al codice del
Listato 3-5.
fn main() { let a = [10, 20, 30, 40, 50]; for elemento in a { println!("il valore è: {elemento}"); } }
for
Quando eseguiamo questo codice, vedremo lo stesso risultato del Listato 3-4. Ma,
cosa più importante, abbiamo aumentato la sicurezza del codice ed eliminato la
possibilità di bug che potrebbero derivare dall’andare oltre la fine dell’array
o dal non accedere ad ogni elemento dell’array. Il codice macchina generato dai
cicli for
può essere anche più efficiente, perché l’indice non deve essere
confrontato con la lunghezza dell’array a ogni iterazione.
Utilizzando il ciclo for
, non dovrai ricordarti di modificare altro codice se
cambierai il numero di valori nell’array, come invece faresti con il metodo
while
usato nel Listato 3-4.
La sicurezza e la concisione dei cicli for
li rendono il costrutto di ciclo
più usato in Rust. Anche nelle situazioni in cui vuoi eseguire un certo numero
di volte il codice, come nell’esempio del conto alla rovescia che utilizzava un
ciclo while
nel Listato 3-3, la maggior parte dei Rustaceani userebbe un ciclo
for
. Il modo per farlo sarebbe quello di usare un Range
, fornito dalla
libreria standard, che genera tutti i numeri in sequenza partendo da un numero e
finendo prima di un altro numero.
Ecco come apparirebbe il conto alla rovescia utilizzando un ciclo for
e un
altro metodo di cui non abbiamo ancora parlato, rev
, per invertire
l’intervallo.
File: src/main.rs
fn main() { for numero in (1..4).rev() { println!("{numero}!"); } println!("PARTENZA!!!"); }
Questo codice è un po’ più carino, vero?
Riassunto
Ce l’hai fatta! Questo capitolo è stato molto impegnativo: hai imparato a
conoscere le variabili, i type di dati scalari e composti, le funzioni, i
commenti, le espressioni if
e i cicli! Per esercitarti con i concetti discussi
in questo capitolo, prova a costruire dei programmi per eseguire le seguenti
operazioni:
- Convertire le temperature tra Fahrenheit e Celsius.
- Generare l’nesimo numero di Fibonacci.
- Stampare il testo del canto natalizio “The Twelve Days of Christmas”, sfruttando la ripetizione della canzone.
Quando sarai pronto per andare avanti, parleremo di un concetto di Rust che non esiste in altri linguaggi di programmazione: la ownership.