Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Rust: aspetti avanzati del controllo di flusso

Rust: analizziamo e approfondiamo i costrutti del linguaggio di programmazione legati al controllo di flusso
Rust: analizziamo e approfondiamo i costrutti del linguaggio di programmazione legati al controllo di flusso
Link copiato negli appunti

Il controllo di flusso sappiamo essere un aspetto fondamentale della programmazione, anche in Rust. Anzi, potremmo dire che è proprio il controllo di flusso che trasforma una sequenza di istruzioni in un vero e proprio programma. Abbiamo già incontrato:

  • i costrutti condizionali tra cui spiccano le istruzioni if e match;
  • i cicli con cui si dettano le condizioni per cui verranno ripetute istruzioni.

In questa lezione, approfondiamo l'argomento muovendoci sempre in queste due macro aree del controllo del flusso ma vedendo più in dettaglio cosa il linguaggio Rust mette a disposizione.

Assegnazione condizionale: if let in Rust

Abbiamo già studiato che in alcuni casi è necessario gestire dei valori opzionali ovvero che, a seconda delle condizioni, potrebbero non essere disponibili. Ad esempio, abbiamo già incontrato questa definizione di funzione che calcola la divisione tra due numeri. Essa restituirà sempre un risultato a meno che non si abbia il divisore impostato a zero. In quest'ultimo caso la risposta sarà None ovvero valore nullo.

Se abbiamo bisogno che una funzione restituisca un valore o un nullo a seconda dei parametri e delle condizioni della singola invocazione dovremo usare un Option. Ciò comporta però che un elemento di questo tipo dovrà essere gestito con un match o con metodi come unwrap.

Ad esempio, una funzione - già vista in precedenti lezioni - che divide due numeri potrebbe avere un risultato valido o restituire None nel caso in cui il divisore passato sia uguale a zero. Restituire un Option per una funzione del genere è necessario ma ciò richiede di prendersi carico di entrambi i casi con un match:

fn divisione(dividendo: f32, divisore: f32) -> Option<f32> {
    if divisore!=0.0 {Some(dividendo / divisore)} else {None}
}
fn main() {
    let risultato=divisione(9.0,4.0);
    match risultato {
        Some(numero) => {println!("Valore trovato: {}", numero)},
        None => {}
    }
}

Se come in questo caso non ci interessa gestire la risposta None dobbiamo comunque inserire un None => {} in quanto match deve coprire tutti i casi. Eliminandolo infatti otterremmo un errore del tipo error[E0004]: non-exhaustive patterns: `None` not covered.

Per ovviare a strutture di questo tipo, che diventano necessariamente prolisse, possiamo usare l'operatore if let che crea una sorta di assegnazione condizionale che fissa un valore per la variabile solo nel caso in cui non si abbia un risultato nullo:

fn divisione(dividendo: f32, divisore: f32) -> Option<f32> {
    if divisore!=0.0 {Some(dividendo / divisore)} else {None}
}
fn main() {
    if let Some(numero) = divisione(9.0,4.0) {
       println!("Risultato ottenuto: {}", numero);
    }
}

L'assegnazione ha luogo solo nel caso in cui il risultato è un Some.

Assegnazione con alternativa: let else in Rust

Sulla stessa rotta di quanto illustrato nel paragrafo precedente abbiamo anche la possibilità di eseguire l'assegnazione normalmente ma preparando una sorta di piano di fuga nel caso in cui questa non sia possibile. Il costrutto che vediamo ora è let...else e lo utilizziamo con un esempio molto simile al precedente:

fn divisione(dividendo: f32, divisore: f32) -> Option<f32> {
    if divisore!=0.0 {Some(dividendo / divisore)} else {None}
}
fn main() {
    let Some(numero) = divisione(9.0,4.0) else {
       panic!("Dobbiamo interrompere esercizio perchè non c'è alcun risultato");
    };
    println!("Risultato ottenuto: {}", numero);
}

Utilizzeremo la stessa funzione divisione che, come sappiamo, in caso di divisore nullo non restituirà niente o meglio restituirà il caso None per una Option.

Come si vede, la let imposta tutto il necessario per l'assegnazione mentre con l'else viene impostato il da farsi nel caso in cui questa non sia possibile. Infatti passando al momento dell'invocazione un divisore non nullo avremo la stampa di: Risultato ottenuto: 2.25. Come vediamo, ciò che ha generato l'output è il println che segue il costrutto. Pertanto l'esecuzione del let...else è già passata.

Quando si esegue una divisione per zero subentrerà invece il ramo else che attiverà il panic. Come sappiamo esso è un arresto indotto del programma con pubblicazione di un messaggio in output. Se il divisore è zero vedremo infatti l'esecuzione fermarsi e venire pubblicato tra le altre righe:

thread 'main' panicked at src/main.rs:6:8:
Dobbiamo interrompere esercizio perché non c'è alcun risultato

Ciclo condizionale: while else

Concludiamo questa carrellata di costrutti, vedendo la versione "ciclica" di if..let ovvero while...let. Il principio è assolutamente lo stesso: eseguiamo un ciclo che prosegue finché c'è un risultato non nullo.

Immaginiamo la situazione in cui abbiamo due vettori di numeri: uno contiene dei dividendi, l'altro dei divisori. I due vettori avranno la stessa lunghezza (non lo controlliamo nel programma ma lo assumiamo come dato di fatto) e ogni numero nel primo vettore viene diviso per il corrispondente nel secondo: al primo divisore nullo che incontriamo il programma deve arrestarsi.

Possiamo fare così:

fn divisione(dividendo: f32, divisore: f32) -> Option<f32> {
    if divisore!=0.0 {Some(dividendo / divisore)} else {None}
}
fn main() {
    let dividendi=vec![35.0,61.0,7.0,55.0, 99.0];
    let divisori=vec![3.0,4.0,8.0,0.0,6.0];
    let mut contatore =0;
    while let Some(i) = divisione(dividendi[contatore], divisori[contatore]) {
        println!("Risultato: {}", i);
        if contatore<dividendi.len()-1 { contatore+=1 } else { break };
    }
    println!("Operazioni terminate!");
}

La divisione viene invocata con il while let di Rust, un ciclo che si arresta al primo valore None che incontra. All'interno del ciclo, inseriamo un'operazione di incremento del contatore che ovviamente non eccederà i numeri disponibili.

Con i dati che abbiamo immesso, otteniamo questo output:

Risultato: 11.666667
Risultato: 15.25
Risultato: 0.875
Operazioni terminate!

Tre risultati validi nonostante i vettori abbiano entrambi lunghezza cinque: perché? Perché il quarto divisore è nullo pertanto la divisione corrispondente ha risposto None ed il ciclo si è interrotto.

Ti consigliamo anche