SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI
Panic! o Result?
Quando dovresti chiamare panic! e quando dovresti restituire un Result? Quando il codice entra in modalità panico, non c’è modo di recuperare. Potresti chiamare panic! per qualsiasi situazione di errore, che ci sia un modo possibile per recuperare o meno, ma in quel caso stai prendendo la decisione che una situazione è irrecuperabile per conto del codice chiamante. Quando scegli di restituire un valore Result, dai al codice chiamante delle opzioni. Il codice chiamante potrebbe scegliere di tentare di recuperare in un modo appropriato per la sua situazione, oppure potrebbe decidere che un valore Err in questo caso è irrecuperabile, quindi può chiamare panic! e trasformare il tuo errore recuperabile in uno irrecuperabile. Pertanto, restituire Result è una buona scelta predefinita quando stai definendo una funzione che potrebbe fallire.
In situazioni come esempi, codice prototipo e test, è più appropriato scrivere del codice che entra in modalità panico anziché restituire un Result. Esploriamo il motivo, quindi discutiamo delle situazioni in cui il compilatore non può capire che il fallimento è impossibile, ma tu come essere umano puoi. Il capitolo concluderà con alcune linee guida generali su come decidere se entrare in modalità panico nel codice della libreria.
Esempi, Codice Prototipo e Test
Quando scrivi un esempio per illustrare qualche concetto, includere anche del codice di gestione degli errori robusto può rendere l’esempio meno chiaro. Negli esempi, si capisce che una chiamata a un metodo come unwrap che potrebbe entrare in modalità panico è intesa come un segnaposto per il modo in cui desideri che la tua applicazione gestisca gli errori, che possono differire in base a ciò che fa il resto del tuo codice.
Allo stesso modo, i metodi unwrap e expect sono molto utili durante la prototipazione, prima di essere pronti a decidere come gestire gli errori. Lasciano segnalibri chiari nel tuo codice per quando sei pronto a rendere il tuo programma più robusto.
Se una chiamata di metodo fallisce in un test, vorresti che l’intero test fallisse, anche se quel metodo non è la funzionalità sotto test. Poiché panic! è come un test contrassegnato come fallimento, chiamare unwrap o expect è esattamente ciò che dovrebbe accadere.
Casi in Cui Hai Più Informazioni Rispetto al Compilatore
Sarebbe anche appropriato chiamare unwrap o expect quando hai un’altra logica che garantisce che il Result avrà un valore Ok, ma la logica non è qualcosa che il compilatore capisce. Avrai comunque un valore Result che devi gestire: qualsiasi operazione che stai chiamando ha ancora la possibilità di fallire in generale, anche se è logicamente impossibile nella tua situazione particolare. Se puoi assicurarti manualmente ispezionando il codice che non avrai mai una variante Err, è perfettamente accettabile chiamare unwrap, e ancora meglio documentare il motivo per cui pensi di non avere mai una variante Err nel testo di expect. Ecco un esempio:
rust
use std::net::IpAddr;
let home: IpAddr = "127.0.0.1"
.parse()
.expect("L'indirizzo IP codificato deve essere valido");
Stiamo creando un’istanza di IpAddr analizzando una stringa codificata. Possiamo vedere che 127.0.0.1 è un indirizzo IP valido, quindi è accettabile usare expect qui. Tuttavia, avere una stringa codificata, valida non cambia il tipo di ritorno del metodo di analisi: otteniamo ancora un valore Result, e il compilatore ci farà ancora gestire il Result come se la variante Err fosse una possibilità perché il compilatore non è abbastanza intelligente da vedere che questa stringa è sempre un indirizzo IP valido. Se la stringa dell’indirizzo IP provenisse da un utente anziché essere codificata nel programma e quindi avesse una possibilità di fallire, sicuramente vorremmo gestire il Result in modo più robusto. Menzionare l’assunzione che questo indirizzo IP sia codificato ci spingerà a cambiare expect in un codice di gestione degli errori migliore se in futuro dovessimo ottenere l’indirizzo IP da un’altra fonte.
Linee Guida per la Gestione degli Errori
È consigliabile far entrare in modalità panico il tuo codice quando è possibile che il tuo codice possa finire in uno stato cattivo. In questo contesto, uno stato cattivo è quando qualche assunzione, garanzia, contratto o invariante è stato violato, come quando vengono passati al tuo codice valori non validi, valori contraddittori o valori mancanti – più uno o più dei seguenti:
- Lo stato cattivo è qualcosa di inaspettato, rispetto a qualcosa che probabilmente accadrà occasionalmente, come un utente che inserisce dati nel formato sbagliato.
- Il tuo codice dopo questo punto deve fare affidamento sul fatto di non essere in questo stato cattivo, piuttosto che controllare il problema ad ogni passaggio.
- Non c’è un buon modo per codificare queste informazioni nei tipi che usi. Lavoreremo su un esempio di cosa intendiamo nella sezione “Codifica degli Stati e del Comportamento come Tipi” del Capitolo 17.
Se qualcuno chiama il tuo codice e passa valori che non hanno senso, è meglio restituire un errore se puoi in modo che l’utente della libreria possa decidere cosa fare in quel caso. Tuttavia, nei casi in cui continuare potrebbe essere insicuro o dannoso, la scelta migliore potrebbe essere chiamare panic! e avvisare la persona che sta usando la tua libreria del bug nel loro codice in modo che possano correggerlo durante lo sviluppo. Allo stesso modo, panic! è spesso appropriato se stai chiamando codice esterno che è fuori dal tuo controllo e restituisce uno stato non valido che non hai modo di correggere.
Tuttavia, quando il fallimento è previsto, è più appropriato restituire un Result che chiamare panic!. Esempi includono un parser che riceve dati malformati o una richiesta HTTP che restituisce uno stato che indica di aver raggiunto un limite di velocità. In questi casi, restituire un Result indica che il fallimento è una possibilità prevista che il codice chiamante deve decidere come gestire.
Quando il tuo codice esegue un’operazione che potrebbe mettere un utente a rischio se viene chiamato utilizzando valori non validi, il tuo codice dovrebbe verificare prima che i valori siano validi e entrare in modalità panico se i valori non sono validi. Questo è principalmente per motivi di sicurezza: tentare di operare su dati non validi può esporre il tuo codice a vulnerabilità. Questo è il motivo principale per cui la libreria standard chiamerà panic! se si tenta un accesso fuori limiti alla memoria: cercare di accedere alla memoria che non appartiene alla struttura dati corrente è un problema di sicurezza comune. Le funzioni hanno spesso dei contratti: il loro comportamento è garantito solo se gli input soddisfano determinati requisiti. Entrare in modalità panico quando viene violato il contratto ha senso perché una violazione del contratto indica sempre un bug del lato del chiamante e non è un tipo di errore che si desidera che il codice chiamante gestisca esplicitamente. In effetti, non c’è modo ragionevole per il codice chiamante recuperare; i programmatori chiamanti devono correggere il codice. I contratti per una funzione, specialmente quando una violazione provocherà una modalità panico, dovrebbero essere spiegati nella documentazione dell’API per la funzione.
Tuttavia, avere molte verifiche degli errori in tutte le tue funzioni sarebbe verboso e fastidioso. Fortunatamente, puoi utilizzare il sistema di tipi di Rust (e quindi il controllo dei tipi eseguito dal compilatore) per fare molte delle verifiche al posto tuo. Se la tua funzione ha un particolare tipo come parametro, puoi procedere con la logica del tuo codice sapendo che il compilatore ha già garantito di avere un valore valido. Ad esempio, se hai un tipo anziché un’opzione, il tuo programma si aspetta di avere qualcosa anziché nulla. Il tuo codice quindi non deve gestire due casi per le varianti Some e None: avrà solo un caso per avere sicuramente un valore. Il codice che cerca di passare nulla alla tua funzione non verrà nemmeno compilato, quindi la tua funzione non deve controllare quel caso a tempo di esecuzione. Un altro esempio è l’utilizzo di un tipo di intero senza segno come u32, che garantisce che il parametro non sia mai negativo.
Creazione di Tipi Personalizzati per la Validazione
Prendiamo l’idea di utilizzare il sistema di tipi di Rust per garantire di avere un valore valido un passo oltre e vediamo come creare un tipo personalizzato per la validazione. Ricorda il gioco dell’indovinello nel Capitolo 2, in cui il nostro codice chiedeva all’utente di indovinare un numero tra 1 e 100. Non abbiamo mai validato che il numero inserito dall’utente fosse compreso in quei numeri prima di verificarlo con il nostro numero segreto; abbiamo solo verificato che il numero inserito fosse positivo. In questo caso, le conseguenze non erano molto gravi: il nostro output di “Troppo alto” o “Troppo basso” sarebbe comunque corretto. Ma sarebbe un’aggiunta utile guidare l’utente verso indovinamenti validi e avere un comportamento diverso quando un utente inserisce un numero fuori dall’intervallo rispetto a quando un utente inserisce, ad esempio, lettere.
Un modo per farlo sarebbe analizzare il tentativo come un i32 anziché solo un u32 per consentire numeri potenzialmente negativi, e quindi aggiungere un controllo affinché il numero sia nell’intervallo, così:
rust
loop {
// --snip-- let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
}; if guess < 1 || guess > 100 {
println!("Il numero segreto sarà compreso tra 1 e 100.");
continue;
}
match guess.cmp(&secret_number) {
// --snip--
}
L’espressione if controlla se il nostro valore è fuori dall’intervallo, informa l’utente del problema e chiama continue per avviare la prossima iterazione del loop e chiedere un altro tentativo. Dopo l’espressione if, possiamo procedere con i confronti tra il tentativo e il numero segreto sapendo che il tentativo è compreso tra 1 e 100.
Tuttavia, questa non è una soluzione ideale: se fosse assolutamente critico che il programma operasse solo su valori tra 1 e 100, e avesse molte funzioni con questo requisito, avere un controllo del genere in ogni funzione sarebbe noioso (e potrebbe influire sulle prestazioni).
Invece, possiamo creare un nuovo tipo e inserire le validazioni in una funzione per creare un’istanza del tipo anziché ripetere le validazioni ovunque. In questo modo, è sicuro per le funzioni utilizzare il nuovo tipo nelle loro firme e utilizzare con fiducia i valori ricevuti. L’esempio 9-13 mostra un modo per definire un tipo Guess che creerà solo un’istanza di Guess se la nuova funzione riceve un valore compreso tra 1 e 100.
rust
pub struct Guess {
value: i32,
} impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Il valore dell'indovinello deve essere compreso tra 1 e 100, ottenuto {}.", value);
} Guess { value }
}
pub fn value(&self) -> i32 {
self.value
}
}
Prima, definiamo una struttura chiamata Guess che ha un campo chiamato value che contiene un i32. Qui verrà memorizzato il numero.
Poi implementiamo una funzione associata chiamata new su Guess che crea istanze di valori Guess. La funzione new è definita per avere un parametro chiamato value di tipo i32 e restituire un Guess. Il codice nel corpo della funzione new testa il valore per assicurarsi che sia compreso tra 1 e 100. Se il valore non supera questo test, effettuiamo una chiamata panic!, che avviserà il programmatore che sta scrivendo il codice chiamante che ha un bug da risolvere, perché creare un Guess con un valore al di fuori di questo intervallo violerebbe il contratto su cui Guess::new si basa. Le condizioni in cui Guess::new potrebbe entrare in modalità panico dovrebbero essere discusse nella documentazione API pubblica; parleremo delle convenzioni di documentazione che indicano la possibilità di una modalità panico nella documentazione API che crei nel Capitolo 14. Se il valore supera il test, creiamo un nuovo Guess con il suo campo value impostato sul parametro value e restituiamo il Guess.
Successivamente, implementiamo un metodo chiamato value che prende in prestito self, non ha altri parametri e restituisce un i32. Questo tipo di metodo è a volte chiamato getter, perché il suo scopo è ottenere alcuni dati dai suoi campi e restituirli. Questo metodo pubblico è necessario perché il campo value della struttura Guess è privato. È importante che il campo value sia privato in modo che il codice che utilizza la struttura Guess non sia autorizzato a impostare value direttamente: il codice esterno al modulo deve utilizzare la funzione Guess::new per creare un’istanza di Guess, garantendo così che non ci sia modo per un Guess di avere un valore che non sia stato controllato dalle condizioni nella funzione Guess::new.
Una funzione che ha un parametro o restituisce solo numeri tra 1 e 100 potrebbe quindi dichiarare nella sua firma che prende o restituisce un Guess anziché un i32 e non avrebbe bisogno di fare ulteriori controlli nel suo corpo.
Sommario
Le funzionalità di gestione degli errori di Rust sono progettate per aiutarti a scrivere codice più robusto. La macro panic! segnala che il tuo programma si trova in uno stato che non può gestire e ti consente di far fermare il processo anziché cercare di procedere con valori non validi o incorretti. L’enum Result utilizza il sistema di tipi di Rust per indicare che le operazioni potrebbero fallire in un modo da cui il tuo codice potrebbe recuperare. Puoi utilizzare Result per informare il codice che chiama il tuo codice che deve gestire potenziali successi o fallimenti. Utilizzare panic! e Result nelle situazioni appropriate renderà il tuo codice più affidabile di fronte a problemi inevitabili.
Ora che hai visto modi utili in cui la libreria standard utilizza i generici con le enum Option e Result, parleremo di come funzionano i generici e come puoi usarli nel tuo codice.