Errori personalizzati in Rust
Nella pagina su Result, ogni errore era semplicemente una String. Per piccoli esempi va benissimo, ma non regge più quando una funzione può fallire in più di un modo distinto: su una String non puoi fare match, quindi chi chiama non può sapere quale tipo di fallimento è avvenuto senza analizzare il testo.
Il problema degli errori String
Sezione intitolata “Il problema degli errori String”fn withdraw(balance: f64, amount: f64) -> Result<f64, String> { if amount > balance { return Err(String::from("fondi insufficienti")); } if amount < 0.0 { return Err(String::from("l'importo non può essere negativo")); } Ok(balance - amount)}Se chi chiama vuole reagire in modo diverso a «fondi insufficienti» e a «importo negativo», l’unica possibilità è confrontare il testo dell’errore: una soluzione fragile, che si rompe appena riformuli un messaggio.
Definire un errore come enum
Sezione intitolata “Definire un errore come enum”Un’enum risolve il problema: ogni tipo di fallimento diventa una variante a sé, su cui chi chiama può fare direttamente match:
enum WithdrawError { InsufficientFunds, NegativeAmount,}
fn withdraw(balance: f64, amount: f64) -> Result<f64, WithdrawError> { if amount < 0.0 { return Err(WithdrawError::NegativeAmount); } if amount > balance { return Err(WithdrawError::InsufficientFunds); } Ok(balance - amount)}
fn main() { match withdraw(100.0, 150.0) { Ok(remaining) => println!("Nuovo saldo: {remaining}"), Err(WithdrawError::InsufficientFunds) => println!("Non ci sono abbastanza soldi"), Err(WithdrawError::NegativeAmount) => println!("Non puoi prelevare un importo negativo"), }}Output:
Non ci sono abbastanza soldiOra è il compilatore a garantire che ogni tipo di fallimento venga gestito: è la stessa garanzia che match ti ha sempre dato con le enum, solo applicata agli errori.
Implementare Display per un messaggio di errore come si deve
Sezione intitolata “Implementare Display per un messaggio di errore come si deve”Prova adesso a stampare WithdrawError con {}: non compila, perché Rust non sa come trasformarlo in testo. Implementare il trait Display risolve il problema. I trait avranno presto pagine tutte loro (Trait e Trait comuni). Per ora puoi copiare questo schema così com’è:
use std::fmt;
enum WithdrawError { InsufficientFunds, NegativeAmount,}
impl fmt::Display for WithdrawError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { WithdrawError::InsufficientFunds => write!(f, "fondi insufficienti"), WithdrawError::NegativeAmount => write!(f, "l'importo non può essere negativo"), } }}
fn main() { let error = WithdrawError::InsufficientFunds; println!("Errore: {error}");}Output:
Errore: fondi insufficientiOra WithdrawError si comporta come qualsiasi altro errore che vorresti mostrare a un utente o scrivere in un log, e resta comunque possibile farci match nel codice che deve reagire a un caso preciso.
Quando un crate può aiutarti
Sezione intitolata “Quando un crate può aiutarti”Scrivere a mano Display e tutto il codice ripetitivo attorno a un tipo di errore personalizzato diventa noioso in un progetto vero. Il crate thiserror (in inglese) ne genera quasi tutto con un #[derive], e anyhow (in inglese) è molto usato nel codice applicativo che deve solo propagare gli errori senza definire un tipo per ognuno. Vale la pena sapere che esistono, ma non ti servono per imparare le basi. Quando sei pronto, la pagina anyhow e thiserror li mostra entrambi in azione e spiega quando scegliere l’uno o l’altro.
Pratica
Sezione intitolata “Pratica”1. Errori di login
Sezione intitolata “1. Errori di login”Definisci un’enum LoginError con due varianti, EmptyUsername e PasswordTooShort, e implementa Display per lei. Poi scrivi fn check_login(user: &str, password: &str) -> Result<String, LoginError>, che restituisce Ok con un messaggio di benvenuto, oppure l’errore giusto. Una password deve avere almeno 8 caratteri. Suggerimento: .is_empty() e .len() su un &str ti danno tutto quello che serve per i controlli.
Mostra la soluzione
use std::fmt;
enum LoginError { EmptyUsername, PasswordTooShort,}
impl fmt::Display for LoginError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { LoginError::EmptyUsername => write!(f, "il nome utente non può essere vuoto"), LoginError::PasswordTooShort => write!(f, "la password deve avere almeno 8 caratteri"), } }}
fn check_login(user: &str, password: &str) -> Result<String, LoginError> { if user.is_empty() { return Err(LoginError::EmptyUsername); } if password.len() < 8 { return Err(LoginError::PasswordTooShort); } Ok(format!("Benvenuta, {user}!"))}
fn main() { let attempts = [("ana", "correct-horse"), ("", "secret123"), ("ben", "short")];
for (user, password) in attempts { match check_login(user, password) { Ok(message) => println!("{message}"), Err(e) => println!("Errore: {e}"), } }}Output:
Benvenuta, ana!Errore: il nome utente non può essere vuotoErrore: la password deve avere almeno 8 caratteriLe password di prova restano in inglese e senza accenti: .len() conta i byte, e una lettera accentata ne occupa due.
2. Errori che portano dettagli
Sezione intitolata “2. Errori che portano dettagli”Scrivi fn parse_percent(input: &str) -> Result<u32, PercentError>, dove PercentError ha due varianti che contengono dati: NotANumber(String) (il testo sbagliato) e TooLarge(u32) (il numero che superava 100). Fai in modo che i messaggi di Display includano questi dettagli. Suggerimento: le varianti di un’enum possono contenere valori, come nella pagina sulle enum. In fmt, fai match con PercentError::TooLarge(n) => ... per estrarre il valore.
Mostra la soluzione
use std::fmt;
enum PercentError { NotANumber(String), TooLarge(u32),}
impl fmt::Display for PercentError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { PercentError::NotANumber(text) => write!(f, "'{text}' non è un numero"), PercentError::TooLarge(n) => write!(f, "{n} è più di 100"), } }}
fn parse_percent(input: &str) -> Result<u32, PercentError> { match input.parse::<u32>() { Ok(n) => { if n > 100 { Err(PercentError::TooLarge(n)) } else { Ok(n) } } Err(_) => Err(PercentError::NotANumber(input.to_string())), }}
fn main() { for input in ["75", "metà", "250"] { match parse_percent(input) { Ok(n) => println!("{n}%"), Err(e) => println!("Errore: {e}"), } }}Output:
75%Errore: 'metà' non è un numeroErrore: 250 è più di 100Riepilogo
Sezione intitolata “Riepilogo”- Un errore
Stringva bene per piccoli esempi, ma chi chiama non può distinguere i casi conmatch. - Definisci un’enum con una variante per ogni tipo di fallimento, così chi chiama può fare
matchsull’errore preciso. - Implementa
Display(impl fmt::Display for TuoErrore) per poter stampare il tipo di errore con{}. - Nei progetti più grandi, crate come
thiserroreanyhoweliminano quasi tutto il codice ripetitivo.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Perché un tipo di errore String non basta più quando una funzione può fallire in più modi?
2. Cosa permette a chi chiama withdraw di reagire in modo diverso a InsufficientFunds e a NegativeAmount?
3. Cosa ti permette di fare l'implementazione di Display per un tipo di errore personalizzato?
4. In cosa aiutano crate come thiserror e anyhow?
5. Perché confrontare il testo grezzo dei messaggi di errore è considerato fragile?
Punteggio: 0 / 5
Per approfondire
Sezione intitolata “Per approfondire”- Defining an error type — Rust by Example (in inglese): un esempio breve ed eseguibile di tipo di errore personalizzato con
Display. - Wrapping errors — Rust by Example (in inglese): il passo successivo, un’enum di errore che avvolge altri errori e funziona con
?grazie aFrom. std::error::Error— documentazione std (in inglese): il trait standard che implementano i tipi di errore «veri», e a cosa serve il suo metodosource().- To panic! or Not to panic! — The Rust Book (in inglese): linee guida su quando restituire un errore e quando fermare il programma.
- thiserror — docs.rs (in inglese): il crate che deriva
DisplayedErrorper le tue enum di errore, così scrivi molto meno codice.