|
Gestire gli errori è un arte complessa. I metodi sono tanti a causa della diversità di scenari che possono presentarsi. In questo capitolo proveremo a fare un po' il quadro di quanto Rust ci mette a disposizione introducendo anche una importante novità. Tenete presente che si tratta di un argomento davvero complesso, la guida ufficiale suggerisce delle linee guida che vi consiglio caldamente di consultare. Chi viene da altri linguaggi come C# o Java non troverà nulla che assomigli al meccanismo try-catch (che a me piace abbastanza) di quei linguaggi. Riprendiamo il discorso degli errori recuperabili ampliando un po' le conoscenze sullo strumento principe, ovvero result. Vediamo subito l'esempio:
Questo programma cerca di aprire un file che non esiste e allora va in errore. Questo errore viene gestito alla riga 5 e presenta il seguente output:
La stessa riga 5 presenta la macro eprintln! che in Rust serve per stampare messaggi di errore (o altre informazioni diagnostiche) direttamente sul flusso di errore standard (standard error, o stderr), anziché sul flusso di output standard (stdout) dal quale potrebbe anche essere reindirizzato. E' possibile anche usare la sintassi di propagazione dell'errore (questo sistema funziona solo in congiunzione con Result) che fa uso di ?. Vediamo un esempio che lo usa e la sua alternativa "normale":
In termini di funzionalità non cambia nulla ma nel primo caso il codice è più conciso e per casi più complicati certamente più leggibile. Su Result abbiamo detto già molto, non resta che esercitarsi con la pratica e i numerosi esempi disponibili online. Una cosa però che non è facilmente reperibile, sempre a proposito di Result, è che abbiamo la possibilità di gestire errori di tipo diverso. Ovvero potrebbe essere indicato presentare messaggi diversi a seconda del tipo di anomalia riscontrata dal programma. Per esempio affrontiamo il seguente problema: vogliamo che l'utente fornisca in input un numero che sia pari e minore di 10. Vogliamo inoltre gestire in maniera differenziata le tre possibili condizioni di errore: 1) il numero fornito è dispari 2) il numero fornito è > 10. 3) l'utente non inserisce un numero In questo caso, la strada che si può usare, non è l'unico credo ma certamente è quella che conosco io, fa uso di un enumeratore personalizzato. Vediamo il programma che risolve il problema:
Quindi, alla riga 3 definiamo il nostro enumerativo che espone i 3 tipi di errore che vogliamo isolare. La funzione valida_input è il cuore del programma in particolasre la riga 9 ci propone, oltre ai consueti trim per eliminare gli spazi e parse che effettua il parsing, la mappatura dell'errore, quale che esso sia, restituendo l'errore da noi definito tramite l'enumeratore. Avrete notato il ? per la propagazione dell'errore. Nelle righe immediatamente successive si specifica quello corretto a seconda della situazione di errore incontrata. Questo è un esempio semplice ma la tecnica può essere applicata anche in casi più complessi. Per cultura, ma non solo vi consiglio di dare uno sguardo all'enumeratore ErrorKind che propone numerose varianti che coprono una vasta gamma (41 al momento in cui scrivo) di errori relativi al I/O. E' il momento adesso di parlare degli errori non recuperabili. Si tratta ovviamente di situazioni al limite e normalmente non dovrete ricorrere al costrutto che stiamo per presentare il quale è sconsigliato per gestire errori recuperabili in altro modo. La macro panic! in Rust è il meccanismo utilizzato per gestire situazioni di errore che non possono essere recuperate. Quando viene invocata, panic! stampa un messaggio di errore, svolge l’operazione di unwind dello stack, ovvero scarica i dati dalla pila che costituisce lo stack, pulisce le risorse eventualmente occupate (quelle che si trovano sullo heap tramite chiamate a drop) e infine termina il programma. È tipicamente usata come detto in scenari dove continuare l’esecuzione del programma potrebbe portare a comportamenti scorretti o corruzione dei dati a fronte di situazioni imprevedibili. In questo paragrafo facciamo solo un breve accenno ad esso. Schematicamente si ha:
Ovviamente questa macro deve essere usata cum grano salis perchè termina bruscamente l'applicativo che sta girando, pertanto è bene ribadire che è da prendere in cosiderazione solo in casi particolarmente critici. Esempio banale: fn main() { panic!("Sono nel panico!"); } Come detto panic! risale lungo lo stack e chiude tutto per liberare risorse. Un altro esempio un po' meno banale (ma sempre non consigliabile da ripetere nella pratica) è ad esempio la seguente funzione: fn divisione(a: i32, b: i32) -> i32 { if b == 0 { panic!("Errore: divisione per zero!"); } a / b } E' possibile intercettare tale sequenza e bloccarla, pratica peraltro piuttosto rara e complicata, per quanto ne so. Essa è possibile tramite un meccanismo noto come catch_unwind, il nome dice tutto. La funzione panic::catch_unwind in Rust è utilizzata per catturare e gestire i panic che avvengono durante l’unwind dello stack1. Quando un panic si verifica, Rust di solito inizia a “srotolare” lo stack, terminando i thread e pulendo le risorse. Tuttavia, catch_unwind permette di intercettare questo processo e di reagire in modo controllato. Ecco cosa fa catch_unwind: 1) Invoca una closure: Esegue una funzione anonima (closure) fornita come argomento. 2) Cattura il panic: Se la closure va in panic, catch_unwind cattura l’oggetto del panic. 3) Ritorna un Result: Se la closure non va in panic, catch_unwind ritorna Ok con il risultato della closure. Se invece la closure va in panic, ritorna Err con l’oggetto del panic. Vediamo di seguito un esempio:
che restituisce come output:
che come si vede porta a termine il programma. Questa pratica non è molto comune, è un po' a metà strada tra result e il panic! vero e proprio. Il discorso sugli errori comprende deve abbracciare ovviamente tutto quanto avevamo visto nel capitolo relativo agli overflow che vi invito a riguardare. Anche quella è una modalità alternativa per gestire casi critici che non va dimenticata. Così come da non dimenticare è anche la modalità che fa uso di expect e unwrap per gestire gli errori di tipo Result. Abbiamo incontrato entrambi ad esempio parlando della gestione dell'input. Quando ad esempio si richiede che questo sia di tipo numerico, rivediamo il codice dell'esempio 18.1: use std::io; use std::io::prelude::*; fn main(){ print!( "Inserisci un numero: "); io::stdout().flush().ok().expect(""); let mut input01 = String::new(); io::stdin().read_line(&mut input01); let x01: i32 = input01.trim().parse() .ok() .expect("Voglio un numero!"); if x01 > 10 { print!("Inserito numero > 10"); } } in questo caso abbiamo usato expect (quello evidenziato in rosso è l'elemento che ci interessa) che ci permette di esporre un avviso che chiarisce perchè il programma è andato in panico. Si poteva anche usare unwrap ma l'esito sarebbe stato leggermente diverso con messaggio di errore più criptico in caso di input non adeguato al formato numerico. in pratica unwrap è da usare solo se siete praticamente sicuri che l'operazione non fallirà, expect in tutti i casi in cui vi sia un dubbio così da poter chiarire esplicimtamente cosa è andato storto. Presento. dal momento che si tratta di situazioni comuni, un programma completo che ci permette di gestirle tramite i costrutti che abbiamo già visto evitando il panic che termina il programma:
Questo semplice codice chiede all'utente di insistere finchè non viene inserito il giusto formato di input. Alla riga 6 abbiamo la cattura dell'input e alla riga 7 la gestione di tale input per verificarne il formato tramite match. UN PO' DI TEORIA Finora abbiamo dato un taglio molto pratico a questa sezione. Vale però la pena di sapere, sia per scopi "culturali" sia per scopi pratici, in quanto queste cose prima o poi vi torneranno utili, ovvero che struct dedicate agli errori sono presenti in molti trait. Ad esempio, non esaustivo, abbiamo: -- std::ftm::Error che restituisce il tipo di errore proveniente da una formattazione -- std::io::Error che riporta ovviamente errori di I/O -- std::str:UTF8Error relativa all'interpretazione di stringhe in formato UTF8 -- std::num::ParseIntError errore in fase di parsing di un intero -- std::num::ParseFloatError errore in fase di parsing di un numero con virgola Vediamo un esempio basilare:
che ovviamente presenta un errore considerando che 42a non è un intero valido. |