Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

File: src/main.rs
fn main() {
    let condizione = true;
    let numero = if condizione { 5 } else { 6 };

    println!("Il valore di numero è: {numero}");
}
Listato 3-2: Assegnazione del risultato di un’espressione if as una variabile

La 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.

File: src/main.rs
fn main() {
    let mut numero = 3;

    while numero != 0 {
        println!("{numero}!");

        numero -= 1;
    }

    println!("PARTENZA!!!");
}
Listato 3-3: Utilizzo di un ciclo 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.

File: src/main.rs
fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut indice = 0;

    while indice < 5 {
        println!("il valore è: {}", a[indice]);

        indice += 1;
    }
}
Listato 3-4: Passare in rassegna gli elementi di una collezione con un ciclo 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.

File: src/main.rs
fn main() {
    let a = [10, 20, 30, 40, 50];

    for elemento in a {
        println!("il valore è: {elemento}");
    }
}
Listato 3-5: Passare in rassegna gli elementi di una collezione con un ciclo 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.