Flusso di Controllo

La capacità di eseguire un po' di codice a seconda se una condizione sia true e di eseguire un po' di codice ripetutamente mentre una condizione è true sono blocchi di costruzione di base 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 a seconda delle condizioni. Tu fornisci una condizione e poi dichiari: "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 projects per esplorare l'espressione if. Nel file src/main.rs, inserisci il seguente codice:

Nome del file: src/main.rs

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

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. Poniamo il blocco di codice da eseguire se la condizione è true immediatamente dopo la condizione all'interno delle parentesi graffe. I blocchi di codice associati alle condizioni nelle espressioni if a volte sono chiamati bracci, proprio come i bracci nelle espressioni match di cui abbiamo parlato nella sezione “Comparare la Predizione con il Numero Segreto” del Capitolo 2.

Facoltativamente, possiamo anche includere un'espressione else, come abbiamo scelto di fare qui, per dare al programma un blocco alternativo di codice da eseguire se la condizione risulta false. Se non si fornisce un'espressione else e la condizione è false, il programma salterà semplicemente il blocco if e passerà alla prossima porzione di codice.

Prova a eseguire questo codice; dovresti vedere il seguente output:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition was true

Proviamo a cambiare il valore di number a un valore che rende la condizione false per vedere cosa succede:

fn main() {
    let number = 7;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

Esegui di nuovo il programma e guarda l'output:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
condition was false

Vale anche la pena 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:

Nome del file: src/main.rs

fn main() {
    let number = 3;

    if number {
        println!("number was three");
    }
}

La condizione if questa volta valuta un valore di 3, e Rust lancia un errore:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
 --> src/main.rs:4:8
  |
4 |     if number {
  |        ^^^^^^ expected `bool`, found integer

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` due to previous error

L'errore indica che Rust si aspettava un bool ma ha ottenuto un integer. A differenza di linguaggi come Ruby e JavaScript, Rust non tenterà automaticamente di convertire tipi non booleani in un Booleano. Devi essere esplicito e fornire sempre if con un Booleano come 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 modo seguente:

Nome del file: src/main.rs

fn main() {
    let number = 3;

    if number != 0 {
        println!("number was something other than zero");
    }
}

Eseguire questo codice stamperà number was something other than zero.

Gestione di Più Condizioni con else if

Puoi usare più condizioni combinando if e else in un'espressione else if. Ad esempio:

Nome del file: src/main.rs

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

Questo programma ha quattro possibili percorsi che può seguire. Dopo averlo eseguito, dovresti vedere il seguente output:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
number is divisible by 3

Quando questo programma viene eseguito, verifica ogni espressione if a turno ed esegue il primo corpo per cui la condizione valuta true. Nota che anche se 6 è divisibile per 2, non vediamo l'output number is divisible by 2, né vediamo il testo number is not divisible by 4, 3, or 2 dal blocco else. Questo perché Rust esegue solo il blocco per la prima condizione true, e una volta trovata una, non verifica nemmeno le restanti.

Usare troppe espressioni else if può rendere il tuo codice ingombrante, quindi se hai più di una, potresti voler rifattorizzare il tuo codice. Il Capitolo 6 descrive un potente costrutto di ramificazione di Rust chiamato match per questi casi.

Uso di if in una Dichiarazione let

Poiché if è un'espressione, possiamo usarla sul lato destro di una dichiarazione let per assegnare il risultato a una variabile, come mostrato nell'Elenco 3-2.

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("The value of number is: {number}");
}

La variabile number sarà legata a un valore basato sul risultato dell'espressione if. Esegui questo codice per vedere cosa succede:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/branches`
The value of number is: 5

Ricorda che i blocchi di codice valutano l'ultima espressione al loro interno, e i numeri da soli sono anche espressioni. In questo caso, il valore dell'intera espressione if dipende da quale blocco di codice viene eseguito. Questo significa che i valori che hanno il potenziale di essere risultati di ogni braccio dell'espressione if devono essere dello stesso tipo; nell'Elenco 3-2, i risultati di entrambi i bracci if e else erano numeri interi i32. Se i tipi non corrispondono, come nel seguente esempio, otterremo un errore:

Nome del file: src/main.rs

fn main() {
    let condition = true;

    let number = if condition { 5 } else { "six" };

    println!("The value of number is: {number}");
}

Quando proveremo a compilare questo codice, otterremo un errore. I bracci if e else hanno tipi di valore incompatibili, e Rust indica esattamente dove trovare il problema all'interno del programma:

$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:4:44
  |
4 |     let number = if condition { 5 } else { "six" };
  |                                 -          ^^^^^ expected integer, found `&str`
  |                                 |
  |                                 expected because of this

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` due to previous error

L'espressione nel blocco if valuta a un numero intero, e l'espressione nel blocco else valuta a una stringa. Questo non funzionerà perché le variabili devono avere un singolo tipo, e Rust ha bisogno di sapere al momento della compilazione di quale tipo è la variabile number, in modo definitivo. Conoscere il tipo di number permette al compilatore di verificare che il tipo sia valido ovunque utilizziamo number. Rust non sarebbe in grado di farlo se il tipo di number fosse determinato solo a runtime; il compilatore sarebbe più complesso e farebbe meno garanzie sul codice se dovesse tenere traccia di più tipi ipotetici per qualsiasi variabile.

Ripetizione con i Cicli

Spesso è utile eseguire un blocco di codice più di una volta. Per questo compito, Rust fornisce diversi cicli, che eseguiranno il codice all'interno del corpo del ciclo fino alla fine e poi ripartiranno immediatamente dall'inizio. Per sperimentare con i cicli, creiamo un nuovo progetto chiamato loops.

Rust ha tre tipi di cicli: loop, while, e for. Proviamoli tutti.

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

Come esempio, cambia il file src/main.rs nella tua directory loops per assomigliare a questo:

Nome del file: src/main.rs

fn main() {
    loop {
        println!("again!");
    }
}

Quando eseguiamo questo programma, vedremo again! stampato più e più volte continuamente finché non interrompiamo manualmente il programma. La maggior parte dei terminali supporta la scorciatoia da tastiera ctrl-c per interrompere un programma che è bloccato in un ciclo continuo. Prova:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.29s
     Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!

Il simbolo ^C rappresenta dove hai premuto ctrl-c. Potresti vedere o meno la parola again! stampata dopo il ^C, a seconda di dove il codice si trovava nel ciclo quando ha ricevuto il segnale di interruzione.

Fortunatamente, Rust fornisce anche un modo per uscire da un ciclo utilizzando il codice. Puoi posizionare la parola chiave break all'interno del ciclo per dire al programma quando smettere di eseguire il ciclo. Ricorda che abbiamo fatto questo nel gioco delle ipotesi nella sezione “Uscire Dopo un'Ipotesi Corretta” del Capitolo 2 per uscire dal programma quando l'utente ha vinto il gioco indovinando il numero corretto.

Abbiamo anche usato continue nel gioco delle ipotesi, che in un ciclo dice al programma di saltare qualsiasi codice rimanente in questa iterazione del ciclo e andare alla successiva iterazione.

Ritornare Valori dai Cicli

Uno degli usi di un loop è riprovare un'operazione che sai potrebbe fallire, come verificare se un thread ha completato il suo lavoro. Potresti anche aver bisogno di passare il risultato di quell'operazione fuori dal ciclo al resto del tuo codice. Per fare ciò, puoi aggiungere il valore che vuoi ritornato dopo l'espressione break che usi per fermare il ciclo; quel valore verrà restituito fuori dal ciclo in modo che tu possa usarlo, come mostrato qui:

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}

Prima del ciclo, dichiariamo una variabile chiamata counter e la inizializziamo a 0. Poi dichiariamo una variabile chiamata result per contenere il valore restituito dal ciclo. Ad ogni iterazione del ciclo, aggiungiamo 1 alla variabile counter, e poi verifichiamo se il counter è 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.

Puoi anche utilizzare return all'interno di un ciclo. Mentre break esce solo dal ciclo corrente, return esce sempre dalla funzione corrente.

Etichette di Ciclo per Disambiguare tra Più Cicli

Se hai cicli all'interno di cicli, break e continue si applicano al ciclo più interno in quel punto. Puoi facoltativamente specificare un'etichetta di ciclo su un ciclo che puoi usare poi con break o continue per specificare che quelle parole chiave si applicano al ciclo etichettato anziché al ciclo più interno. Le etichette di ciclo devono iniziare con un singolo apostrofo. Ecco un esempio con due cicli innestati:

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 conterà da 0 a 2. Il ciclo interno senza etichetta conta a ritroso 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:

$ 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 avrà spesso bisogno di valutare una condizione all'interno di un ciclo. Mentre la condizione è true, il ciclo si esegue. Quando la condizione cessa di essere true, il programma chiama break, interrompendo il ciclo. È possibile implementare un comportamento come questo utilizzando una combinazione di loop, if, else, e break; puoi provare a farlo ora in un programma, se vuoi. Tuttavia, questo pattern è così comune che Rust ha un costrutto integrato per esso, chiamato ciclo while. Nell'Elenco 3-3, usiamo while per eseguire il programma tre volte, contando ogni volta, e poi, dopo il ciclo, stampare un messaggio ed uscire.

fn main() {
    let mut number = 3;

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

        number -= 1;
    }

    println!("LIFTOFF!!!");
}

Questo costrutto elimina molta nidificazione che sarebbe necessaria se avessi usato loop, if, else, e break, ed è più chiaro. Mentre una condizione valuta come true, il codice viene eseguito; altrimenti, esce dal ciclo.

Ciclare Attraverso una Collezione con for

Puoi anche usare il costrutto while per cicli sugli elementi di una collezione, come un array. Ad esempio, il ciclo nell'Elenco 3-4 stampa ciascun elemento nell'array a.

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}

Qui, il codice conta attraverso gli elementi nell'array. Inizia dall'indice 0, e quindi cicla finché non raggiunge l'indice finale nell'array (cioè, quando index < 5 non è più true). Eseguendo questo codice, stamperà ogni elemento nell'array:

$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.32s
     Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50

Tutti e cinque i valori dell'array compaiono nel terminale, come previsto. Anche se index raggiungerà un valore di 5 ad un certo punto, il ciclo smette di eseguire prima di tentare di recuperare un sesto valore dall'array.

Tuttavia, questo approccio è incline a errori; potremmo far sì che il programma vada in panic se il valore dell'indice o la condizione di verifica è errata. Ad esempio, se cambi il radicamento dell'array a per avere quattro elementi ma ti dimentichi di aggiornare la condizione a while index < 4, il codice andrebbe in panic. È anche lento, perché il compilatore aggiunge runtime di codice per eseguire il controllo condizionale sel'indice è all'interno dei limiti dell'array ad ogni iterazione attraverso il ciclo.

Come alternativa più concisa, puoi utilizzare un ciclo for ed eseguire del codice per ciascun elemento in una collezione. Un ciclo for sembra il codice nell'Elenco 3-5.

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {element}");
    }
}

Quando eseguiamo questo codice, vedremo lo stesso output dell'Elenco 3-4. Più importante, abbiamo ora aumentato la sicurezza del codice ed eliminato la possibilità di bug che potrebbero derivare dall'andare oltre la fine dell'array o dal non andare abbastanza lontano e perdere alcuni elementi.

Utilizzando il ciclo for, non devi ricordarti di cambiare alcun altro codice se cambi il numero di valori nell'array, come faresti con il metodo usato nell'Elenco 3-4.

La sicurezza e la concisione dei cicli for li rendono la struttura di ciclo più comunemente usata in Rust. Anche in situazioni in cui si desidera eseguire un certo numero di volte un certo codice, come nell'esempio del conto alla rovescia che utilizzava un ciclo while nel Listing 3-3, la maggior parte dei Rustaceans userebbero un ciclo for. Il modo per farlo è utilizzare un Range, fornito dalla libreria standard, che genera tutti i numeri in sequenza a partire da un numero e terminando prima di un altro numero.

Ecco come apparirebbe il conto alla rovescia usando un ciclo for e un altro metodo di cui non abbiamo ancora parlato, rev, per invertire l'intervallo:

Nome del file: src/main.rs

fn main() {
    for number in (1..4).rev() {
        println!("{number}!");
    }
    println!("LIFTOFF!!!");
}

Questo codice è un po' più bello, vero?

Ce l'hai fatta! Questo è stato un capitolo considerevole: hai imparato variabili, tipi di dati scalari e composti, funzioni, commenti, espressioni if e cicli! Per esercitarti con i concetti discussi in questo capitolo, prova a costruire dei programmi per fare quanto segue:

  • Convertire le temperature tra Fahrenheit e Celsius.
  • Generare il numero Fibonacci n-esimo.
  • Stampare il testo della canzone natalizia “The Twelve Days of Christmas”, sfruttando la ripetizione nella canzone.

Quando sei pronto per andare avanti, parleremo di un concetto in Rust che non esiste comunemente in altri linguaggi di programmazione: ownership.