SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI
Migliorare la Modularità e la Gestione degli Errori
Per migliorare il nostro programma, affronteremo quattro problemi legati alla struttura del programma e alla gestione degli errori potenziali. Prima di tutto, la nostra funzione principale ora svolge due compiti: analizza gli argomenti e legge i file. Man mano che il nostro programma cresce, il numero di compiti separati che la funzione principale gestisce aumenterà. A mano che una funzione assume più responsabilità, diventa più difficile ragionare su di essa, più difficile da testare e più difficile da modificare senza rompere una delle sue parti. È meglio separare la funzionalità in modo che ogni funzione sia responsabile di un solo compito.
Questo problema è anche legato al secondo problema: anche se query e file_path sono variabili di configurazione del nostro programma, variabili come contents vengono utilizzate per eseguire la logica del programma. Più lunga diventa la funzione main, più variabili dobbiamo portare in scope; più variabili abbiamo in scope, più difficile sarà tenere traccia dello scopo di ciascuna. È meglio raggruppare le variabili di configurazione in una struttura per rendere chiaro il loro scopo.
Il terzo problema è che abbiamo usato expect per stampare un messaggio di errore quando la lettura del file fallisce, ma il messaggio di errore stampa solo “Dovrebbe essere stato possibile leggere il file”. La lettura di un file può fallire in diversi modi: ad esempio, il file potrebbe essere mancante, o potremmo non avere il permesso di aprirlo. Al momento, indipendentemente dalla situazione, stampiamo lo stesso messaggio di errore per tutto, il che non darebbe all’utente alcuna informazione!
Quarto, usiamo expect ripetutamente per gestire errori diversi, e se l’utente esegue il nostro programma senza specificare abbastanza argomenti, otterrà un errore di indice fuori limite da Rust che non spiega chiaramente il problema. Sarebbe meglio se tutto il codice di gestione degli errori fosse in un unico punto in modo che i futuri manutentori dovessero consultare solo un punto del codice se la logica di gestione degli errori dovesse cambiare. Avere tutto il codice di gestione degli errori in un unico punto garantirà anche che stiamo stampando messaggi significativi per i nostri utenti finali.
Affronteremo questi quattro problemi rifattorizzando il nostro progetto. Separazione delle Responsabilità per Progetti Binari
Il problema organizzativo di assegnare la responsabilità per compiti multipli alla funzione main è comune a molti progetti binari. Di conseguenza, la comunità di Rust ha sviluppato linee guida per dividere le diverse responsabilità di un programma binario quando main inizia a diventare grande. Questo processo ha i seguenti passaggi:
- Dividere il programma in un main.rs e un lib.rs e spostare la logica del programma in lib.rs.
- Finché la logica di analisi della riga di comando è piccola, può rimanere in main.rs.
- Quando la logica di analisi della riga di comando inizia a diventare complicata, estraila da main.rs e spostala in lib.rs.
Le responsabilità che rimangono nella funzione main dopo questo processo dovrebbero essere limitate alle seguenti:
- Chiamare la logica di analisi della riga di comando con i valori degli argomenti.
- Impostare qualsiasi altra configurazione.
- Chiamare una funzione run in lib.rs.
- Gestire l’errore se run restituisce un errore.
Questo modello riguarda la separazione delle responsabilità: main.rs gestisce l’esecuzione del programma e lib.rs gestisce tutta la logica del compito in questione. Poiché non puoi testare direttamente la funzione main, questa struttura ti consente di testare tutta la logica del tuo programma spostandola in funzioni in lib.rs. Il codice che rimane in main.rs sarà abbastanza piccolo da verificare la sua correttezza leggendolo. Rilavoriamo il nostro programma seguendo questo processo. Estrazione dell’Analizzatore di Argomenti
Estrarrremo la funzionalità per l’analisi degli argomenti in una funzione che main chiamerà per prepararsi a spostare la logica di analisi della riga di comando in src/lib.rs. La Lista 12-5 mostra il nuovo inizio di main che chiama una nuova funzione parse_config, che definiremo in src/main.rs per il momento.
Trade-Offs dell’Utilizzo di Clone
Nella comunità di Rust, c’è una tendenza a evitare l’uso di clone per risolvere problemi di proprietà a causa del suo costo in tempo di esecuzione. Nel Capitolo 13, imparerai a utilizzare metodi più efficienti in questo tipo di situazione. Ma per ora, va bene copiare alcune stringhe per continuare a fare progressi perché farai queste copie solo una volta e il percorso del file e la stringa di query sono molto piccoli. È meglio avere un programma funzionante che sia un po’ inefficiente che cercare di ottimizzare eccessivamente il codice al primo passaggio. Man mano che diventi più esperto con Rust, sarà più facile iniziare con la soluzione più efficiente, ma per ora, è perfettamente accettabile chiamare clone.
Abbiamo aggiornato il main in modo che inserisca l’istanza di Config restituita da parse_config in una variabile chiamata config, e abbiamo aggiornato il codice che precedentemente utilizzava le variabili query e file_path in modo che ora utilizzi i campi sulla struttura Config.
Ora il nostro codice trasmette più chiaramente che query e file_path sono correlati e che il loro scopo è configurare come funzionerà il programma. Qualsiasi codice che utilizza questi valori sa di trovarli nell’istanza config nei campi denominati per il loro scopo.
Creazione di un Costruttore per Config
Finora abbiamo estratto la logica responsabile dell’analisi degli argomenti da riga di comando da main e l’abbiamo inserita nella funzione parse_config. Ciò ci ha aiutato a capire che i valori di query e file_path erano correlati e che questa relazione dovrebbe essere trasmessa nel nostro codice. Abbiamo quindi aggiunto una struttura Config per denominare lo scopo correlato di query e file_path e per poter restituire i nomi dei valori come nomi dei campi di struttura dalla funzione parse_config.
Ora che lo scopo della funzione parse_config è quello di creare un’istanza di Config, possiamo cambiare parse_config da una semplice funzione a una funzione denominata new associata alla struttura Config. Fare questo cambiamento renderà il codice più idiomatico. Possiamo creare istanze di tipi nella libreria standard, come String, chiamando String::new. Allo stesso modo, cambiando parse_config in una nuova funzione associata a Config, saremo in grado di creare istanze di Config chiamando Config::new. La Lista 12-7 mostra le modifiche che dobbiamo apportare.
Risoluzione della Gestione degli Errori
Ora lavoreremo alla risoluzione della nostra gestione degli errori. Ricorda che tentare di accedere ai valori nel vettore args all’indice 1 o all’indice 2 farà andare in panico il programma se il vettore contiene meno di tre elementi. Prova a eseguire il programma senza argomenti; sembrerà così:
$ cargo run Compilazione minigrep v0.1.0 (file:///projects/minigrep) Target dev finito [non ottimizzato + debuginfo] in 0.0s Esecuzione di target/debug/minigrep
thread ‘main’ in panico a ‘indice fuori limite: la len è 1 ma l’indice è 1’, src/main.rs:27:21 nota: esegui con la variabile d’ambiente RUST_BACKTRACE=1
per visualizzare una traccia dello stack
La riga indice fuori limite: la len è 1 ma l’indice è 1 è un messaggio di errore destinato ai programmatori. Non aiuterà i nostri utenti finali a capire cosa dovrebbero fare invece. Risolviamo questo problema ora.
Miglioramento del Messaggio di Errore
Nella Lista 12-8, aggiungiamo un controllo nella nuova funzione che verificherà che la fetta sia abbastanza lunga prima di accedere all’indice 1 e 2. Se la fetta non è abbastanza lunga, il programma va in panico e visualizza un messaggio di errore migliore.
Questo codice è simile alla funzione Guess::new che abbiamo scritto nella Lista 9-13, dove abbiamo chiamato panic! quando l’argomento del valore era fuori dall’intervallo dei valori validi. Invece di controllare un intervallo di valori qui, controlliamo che la lunghezza di args sia almeno 3 e il resto della funzione può operare sotto l’assunzione che questa condizione sia stata soddisfatta. Se args ha meno di tre elementi, questa condizione sarà vera e chiameremo la macro panic! per terminare immediatamente il programma.
Con queste poche righe extra di codice in new, eseguiamo nuovamente il programma senza alcun argomento per vedere come appare ora l’errore:
$ cargo run Compilazione minigrep v0.1.0 (file:///projects/minigrep) Target dev finito [non ottimizzato + debuginfo] in 0.0s Esecuzione di target/debug/minigrep
thread ‘main’ in panico a ‘non ci sono abbastanza argomenti’, src/main.rs:26:13 nota: esegui con la variabile d’ambiente RUST_BACKTRACE=1
per visualizzare una traccia dello stack
Questa uscita è migliore: ora abbiamo un messaggio di errore ragionevole. Tuttavia, abbiamo anche informazioni superflue che non vogliamo dare ai nostri utenti. Forse utilizzare la tecnica che abbiamo usato nella Lista 9-13 non è la migliore da utilizzare qui: una chiamata a panic! è più appropriata per un problema di programmazione che un problema di utilizzo, come discusso nel Capitolo 9. Invece, utilizzeremo l’altro metodo che hai imparato nel Capitolo 9 – restituendo un Result che indica sia il successo che un errore.
Restituire un Result Invece di Chiamare panic!
Possiamo invece restituire un valore Result che conterrà un’istanza Config nel caso di successo e descriverà il problema nel caso di errore. Modificheremo anche il nome della funzione da new a build perché molti programmatori si aspettano che le nuove funzioni non falliscano mai. Quando Config::build comunica a main, possiamo utilizzare il tipo Result per segnalare che c’è stato un problema. Quindi possiamo cambiare main per convertire una variante Err in un errore più pratico per i nostri utenti senza il testo circostante su thread ‘main’ e RUST_BACKTRACE che una chiamata a panic! provoca.
La Lista 12-9 mostra le modifiche che dobbiamo apportare al valore di ritorno della funzione che ora chiamiamo Config::build e al corpo della funzione necessario per restituire un Result. Nota che questo non verrà compilato fino a quando non aggiorneremo anche main, il che faremo nella prossima lista.