Anche in Rust gli errori in fase di esecuzione sono normali aspetti del funzionamento di un programma. Non dobbiamo pensarli necessariamente come qualcosa "di sbagliato" ma piuttosto come una situazione anomala causata da dati pervenuti dall'esterno o condizioni indotte. Se il nostro programma è in esecuzione significa che sintatticamente è corretto pertanto è stato compilato con successo e lanciato dal runtime.
Eppure, qualcosa di imprevisto può capitare: un software che scarica dati dalla rete può improvvisamente perdere la connessione, lo spazio disponibile del disco su cui stiamo scrivendo file potrebbe esaurirsi e così via.
In questa lezione, scopriamo come il linguaggio Rust permetta di affrontare tali situazioni anomale per poterle gestire ma soprattutto, cosa più importante, evitare che il programma si interrompa in modo incontrollato!
Tipologie di errori I Rust
Scopriamo subito che in Rust si parla di due tipologie di errori: errori unrecoverable e recoverable.
I primi sono quelli che devono decretare la fine del programma mentre i secondi possono essere trattati in maniera alternativa. Nei prossimi due paragrafi, li tratteremo entrambi e soprattutto con le lezioni successive scopriremo molte funzionalità del linguaggio che avranno questa logica di gestione degli errori incorporata. Saranno tutte occasioni propizie per un approfondimento sul tema.
Panic: errori non recuperabili
Nonostante lo scopo sia quello di evitare interruzioni del programma, può capitare che a volte vogliamo noi interrompere volontariamente l'esecuzione in date condizioni. Per questo esiste appositamente la macro panic!
che si occupa di arrestare l'esecuzione, ricevendo come argomento anche un messaggio che sarà proposto tra i log del programma:
fn main() {
println!("Fino a qui tutto bene...");
panic!("...problema...il programma viene interrotto!");
}
Il programma stamperà il messaggio "Fino a qui tutto bene..." e poi si chiuderà per effetto del panic
lasciandone però traccia nel messaggio di uscita:
thread 'main' panicked at src/main.rs:4:5:
...problema...il programma viene interrotto!
Errori recoverable
Nel paragrafo precedente abbiamo scoperto come causare noi stessi l'interruzione del programma, in questo invece vediamo come fare in modo che le operazioni proseguano solo se ci sono le condizioni per farlo. Tipicamente, gli errori sono causati da situazioni in cui una qualche componente con cui collaboriamo restituisce errore o un valore che ci è stato fornito non idoneo allo svolgimento di operazioni. Per fare in modo che il programma prosegua solo in condizioni di sicurezza possiamo basarci su due enum:
Option
che restituisceSome
se c'è un valore disponibile oNone
in caso contrario. In base a questo potremo sapere se abbiamo elementi sufficienti per andare avanti e se affrontare la situazione;Result
che verifica se una determinata operazione ha fornito un risultato o ha dato errore.
Trattandosi di enum
, entrambi i casi possono essere gestiti con match
ma Rust mette anche altre due funzioni che possono fare al caso nostro: unwrap
e expect
.
Usando match
possiamo ricorrere ad un "grande classico" in cui testiamo la divisione per zero:
fn divisione(dividendo: i32, divisore: i32) -> Result<i32, String> {
if divisore != 0 {
Ok(dividendo / divisore)
} else {
Err(String::from("Divisione per zero!"))
}
}
fn main() {
let dividendo=18;
let divisore=3;
let esito = divisione(dividendo, divisore);
match esito {
Ok(risultato) => println!("Operazione {}/{}: {}", dividendo,divisore, risultato),
Err(messaggio) => println!("Errore: {}", messaggio),
}
}
Notiamo che il risultato della funzione è di tipo Result
ma non è affatto scontato. Infatti può trattarsi di un Result
in caso di risultato valido o Err
in caso di divisione per zero. Sarà proprio il match
a giudicare di cosa si tratta in base alla tipologia di riferimento.
Ci potremmo chiedere a questo punto: ma ogni volta che usiamo il Result
(per Option
varrà lo stesso discorso) siamo costretti ad usare il match
? In realtà, no!
Esistono le funzioni unwrap
e expect
che permettono di estrapolare un risultato da una di queste due enum. In pratica, la funzione dell'esempio precedente potrebbe essere invocata anche così:
let esito = divisione(dividendo, divisore);
println!("Operazione {}/{}: {}", dividendo,divisore, esito.unwrap())
o così:
let esito = divisione(dividendo, divisore);
println!("Operazione {}/{}: {}", dividendo,divisore, esito.expect("Errore!"))
Con la differenza che la seconda accetta una stringa da proporre nel caso in cui l'esito non sia favorevole.