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
ematch
; - 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.