Refactoring per Migliorare Modularità e Gestione degli Errori
Per migliorare il nostro programma, risolveremo quattro problemi che riguardano
la struttura del programma e la gestione di potenziali errori. Innanzitutto, la
nostra funzione main
ora esegue due attività: analizza gli argomenti e legge
il file. Man mano che il nostro programma cresce, il numero di attività separate
gestite dalla funzione main
aumenterà. Man mano che una funzione acquisisce
responsabilità, diventa più difficile esaminare, testare e apportare modificare
senza danneggiare una delle sue parti. È meglio separare le funzionalità in modo
che ogni funzione sia responsabile di un’attività.
Questo problema si collega anche al secondo problema: sebbene query
e
percorso_file
siano variabili di configurazione del nostro programma,
variabili come contenuto
vengono utilizzate per eseguire la struttura logica
del programma. Più main
diventa lungo, più variabili dovremo includere nello
scope; più variabili abbiamo nello scope, più difficile sarà tenere traccia
di cosa faccia ciascuna. È meglio raggruppare le variabili di configurazione in
un’unica struttura per chiarirne lo scopo.
Il terzo problema è che abbiamo usato expect
per visualizzare un messaggio di
errore quando la lettura del file fallisce, ma il messaggio di errore visualizza
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 i permessi per aprirlo. Al momento, indipendentemente dalla
situazione, visualizzeremo lo stesso messaggio di errore per tutto, il che non
fornirebbe alcuna informazione all’utente!
In quarto luogo, usiamo expect
per gestire un errore e, se l’utente esegue il
nostro programma senza specificare argomenti sufficienti, riceverà un errore
index out of bounds
da Rust che non spiega chiaramente il problema. Sarebbe
meglio se tutto il codice di gestione degli errori fosse in un unico posto, in
modo che chi in futuro prenderà in mano il nostro codice abbia un solo posto in
cui guardare se la struttura di gestione degli errori avesse bisogno di
cambiamenti. Avere tutto il codice di gestione degli errori in un unico posto
garantirà anche la stampa di messaggi comprensibili per gli utenti della nostra
applicazione.
Affrontiamo questi quattro problemi riscrivendo il nostro progetto.
Separare Attività nei Progetti Binari
Il problema organizzativo di allocare la responsabilità di più attività alla
funzione main
è comune a molti progetti binari. Di conseguenza, molti
programmatori Rust trovano utile suddividere le attività di un programma binario
quando la funzione main
inizia a diventare più grande. Questo processo prevede
i seguenti passaggi:
- Suddividere il programma in un file main.rs e un file lib.rs e spostare la logica del programma in lib.rs.
- Finché la logica di analisi della riga di comando è piccola, può rimanere
nella funzione
main
. - Quando la logica di analisi della riga di comando inizia a complicarsi,
toglierla dalla funzione
main
e metterla in altre funzioni o type.
Le responsabilità che rimangono nella funzione main
dopo questo processo
dovrebbero essere limitate a quanto segue:
- Chiamare la logica di analisi della riga di comando con i valori degli argomenti
- Impostare qualsiasi altra configurazione
- Chiamare una funzione
esegui
in lib.rs - Gestire l’errore se
esegui
restituisce un errore
Questo schema riguarda la separazione delle attività: main.rs gestisce
l’esecuzione del programma e lib.rs gestisce tutta la logica dell’attività in
corso. Poiché non è possibile testare direttamente la funzione main
, questa
struttura consente inoltre di scrivere test e quindi testare tutta la logica del
programma spostandola fuori dalla funzione main
. Il codice che rimane nella
funzione main
sarà sufficientemente piccolo da poterne verificare la
correttezza leggendolo. Ristrutturiamo il nostro programma seguendo questo
processo.
Estrarre il Parser degli Argomenti
Estrarremo la funzionalità per l’analisi degli argomenti in una funzione che
verrà chiamata da main
. Il Listato 12-5 mostra il nuovo avvio della funzione
main
che chiama una nuova funzione leggi_config
, che definiremo in
src/main.rs.
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let (query, percorso_file) = leggi_config(&args);
// --taglio--
println!("Cerco {query}");
println!("Nel file {percorso_file}");
let contenuto = fs::read_to_string(percorso_file)
.expect("Dovrebbe essere stato possibile leggere il file");
println!("Con il testo:\n{contenuto}");
}
fn leggi_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let percorso_file = &args[2];
(query, percorso_file)
}
leggi_config
da main
Stiamo ancora raccogliendo gli argomenti della riga di comando in un vettore, ma
invece di assegnare il valore dell’argomento all’indice 1 alla variabile query
e il valore dell’argomento all’indice 2 alla variabile percorso_file
all’interno della funzione main
, passiamo l’intero vettore alla funzione
leggi_config
. La funzione leggi_config
contiene quindi la logica che
determina quale argomento debba andare in quale variabile e restituisce i valori
a main
. Creiamo ancora le variabili query
e percorso_file
in main
, ma
main
non ha più la responsabilità di determinare come gli argomenti e le
variabili della riga di comando corrispondono.
Questa riscrittura potrebbe sembrare eccessiva per il nostro piccolo programma, ma stiamo eseguendo il refactoring in piccoli passaggi incrementali. Dopo aver apportato questa modifica, esegui nuovamente il programma per verificare che l’analisi degli argomenti funzioni ancora. È consigliabile controllare spesso i progressi per aiutare a identificare la causa dei problemi quando si verificano.
Raggruppare i Valori di Configurazione
Possiamo fare un altro piccolo passo per migliorare ulteriormente la funzione
leggi_config
. Al momento, restituiamo una tupla, ma poi la suddividiamo
immediatamente in singole parti. Questo è un segno che forse non abbiamo ancora
l’astrazione giusta.
Un altro indicatore che mostra che c’è margine di miglioramento è la parte
config
di leggi_config
, che implica che i due valori restituiti sono
correlati e fanno entrambi parte di un unico valore di configurazione. Al
momento non stiamo evidenziando questo significato nella struttura dei dati se
non raggruppando i due valori in una tupla; inseriremo invece i due valori in
un’unica struct e daremo a ciascuno dei campi della struct un nome
significativo. In questo modo, sarà più facile per i futuri manutentori di
questo codice comprendere come i diversi valori si relazionano tra loro e qual è
il loro scopo.
Il Listato 12-6 mostra i miglioramenti alla funzione leggi_config
.
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = leggi_config(&args);
println!("Cerco {}", config.query);
println!("Nel file {}", config.percorso_file);
let contenuto = fs::read_to_string(config.percorso_file)
.expect("Dovrebbe essere stato possibile leggere il file");
// --taglio--
println!("Con il testo:\n{contenuto}");
}
struct Config {
query: String,
percorso_file: String,
}
fn leggi_config(args: &[String]) -> Config {
let query = args[1].clone();
let percorso_file = args[2].clone();
Config { query, percorso_file }
}
leggi_config
per ritornare un’istanza di una struttura Config
Abbiamo aggiunto una struct denominata Config
definita per avere campi
denominati query
e percorso_file
. La firma di leggi_config
ora indica che
ritorna un valore Config
. Nel corpo di leggi_config
, dove prima ritornavamo
slice di stringa che fanno riferimento a valori String
in args
, ora
definiamo Config
in modo che contenga valori String
posseduti. La variabile
args
in main
ha ownership dei valori degli argomenti e consente solo alla
funzione leggi_config
di prenderli in prestito, il che significa che
violeremmo le regole di Rust sui prestiti se Config
tentasse di prendere la
ownership dei valori in args
.
Ci sono diversi modi per gestire i dati String
; il modo più semplice, anche se
un po’ inefficiente, è chiamare il metodo clone
sui valori. Questo creerà una
copia completa dei dati per l’istanza di Config
, che ne diverrà proprietaria,
il che richiede più tempo e memoria rispetto alla memorizzazione di un
reference ai dati stringa. Tuttavia, clonare i dati rende anche il nostro
codice molto semplice perché non dobbiamo gestire la lifetime di quei
reference; in questo caso, rinunciare a un po’ di prestazioni per guadagnare
semplicità è un compromesso che vale la pena accettare.
I Compromessi dell’Utilizzo di Clone
Molti utenti di Rust tendono a evitare di usare clone
per non incorrere in
problemi di ownership a causa del suo costo di esecuzione. Nel Capitolo
13, imparerai come utilizzare metodi più efficienti in
questo tipo di situazioni. Ma per ora, va bene copiare alcune stringhe per
continuare, perché queste copie verranno eseguite solo una volta e il percorso
del file e la stringa di query sono molto piccoli. È meglio avere un programma
funzionante ma un po’ inefficiente che cercare di iper-ottimizzare il codice
al primo tentativo. Man mano che acquisirai esperienza con Rust, sarà più
facile iniziare con la soluzione più efficiente, ma per ora è perfettamente
accettabile chiamare clone
.
Abbiamo aggiornato main
in modo che inserisca l’istanza di Config
restituita
da leggi_config
in una variabile denominata config
, e abbiamo aggiornato il
codice che in precedenza utilizzava le variabili separate query
e
percorso_file
, in modo che ora utilizzi i campi della struct Config
.
Ora il nostro codice comunica più chiaramente che query
e percorso_file
sono
correlati e che il loro scopo è configurare il funzionamento del programma.
Qualsiasi codice che utilizza questi valori sa come trovarli nell’istanza di
config
nei campi denominati appositamente per il loro scopo.
Creare un Costruttore per Config
Finora, abbiamo estratto la logica responsabile dell’analisi degli argomenti
della riga di comando da main
e l’abbiamo inserita nella funzione
leggi_config
. In questo modo abbiamo visto che i valori query
e
percorso_file
erano correlati e che questa relazione doveva essere comunicata
nel nostro codice. Abbiamo quindi aggiunto una struct Config
per denominare
lo scopo correlato di query
e percorso_file
e per poter ritornare i nomi dei
valori come nomi di campo della struct dalla funzione leggi_config
.
Ora che lo scopo della funzione leggi_config
è creare un’istanza di Config
,
possiamo modificare leggi_config
da una semplice funzione a una funzione
chiamata new
associata alla struct Config
. Questa modifica renderà il
codice più idiomatico. Possiamo creare istanze di type nella libreria
standard, come String
, chiamando String::new
. Allo stesso modo, modificando
leggi_config
in una funzione new
associata a Config
, saremo in grado di
creare istanze di Config
chiamando Config::new
. Il Listato 12-7 mostra le
modifiche che dobbiamo apportare.
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Cerco {}", config.query);
println!("Nel file {}", config.percorso_file);
let contenuto = fs::read_to_string(config.percorso_file)
.expect("Dovrebbe essere stato possibile leggere il file");
println!("Con il testo:\n{contenuto}");
// --taglio--
}
// --taglio--
struct Config {
query: String,
percorso_file: String,
}
impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let percorso_file = args[2].clone();
Config { query, percorso_file }
}
}
leggi_config
in Config::new
Abbiamo cambiato in main
dove veniva chiamata leggi_config
con
Config::new
. Abbiamo cambiato la funzione leggi_config
in new
e spostata
nel blocco impl
così da essere associata a Config
. Prova a compilare il
codice per verificare che tutto funzioni come dovrebbe.
Migliorare il Messaggio di Errore
Nel Listato 12-8, aggiungiamo un controllo nella funzione new
che verificherà
che la slice sia sufficientemente lunga prima di accedere agli indici 1 e 2.
Se la slice non è sufficientemente lunga, il programma va in panico e
visualizza un messaggio di errore più chiaro.
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Cerco {}", config.query);
println!("Nel file {}", config.percorso_file);
let contenuto = fs::read_to_string(config.percorso_file)
.expect("Dovrebbe essere stato possibile leggere il file");
println!("Con il testo:\n{contenuto}");
}
struct Config {
query: String,
percorso_file: String,
}
impl Config {
// --taglio--
fn new(args: &[String]) -> Config {
if args.len() < 3 {
panic!("non ci sono abbastanza argomenti");
}
// --taglio--
let query = args[1].clone();
let percorso_file = args[2].clone();
Config { query, percorso_file }
}
}
Questo codice è simile alla funzione Ipotesi::new
che abbiamo scritto nel
Listato 9-13, dove abbiamo chiamato panic!
quando l’argomento valore
era fuori dall’intervallo di valori validi. Invece
di controllare un intervallo di valori, qui controlliamo che la lunghezza di
args
sia di almeno 3
e che il resto della funzione possa funzionare
presupponendo che questa condizione sia stata soddisfatta. Se args
ha meno di
tre elementi, questa condizione sarà true
e chiameremo la macro panic!
per
terminare immediatamente il programma.
Con queste poche righe di codice in new
, eseguiamo di nuovo il programma senza
argomenti per vedere come appare ora l’errore:
$ cargo run
Compiling minigrep v0.1.0 (file:///progetti/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep`
thread 'main' panicked at src/main.rs:26:13:
non ci sono abbastanza argomenti
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Questo output è migliore: ora abbiamo un messaggio di errore ragionevole.
Tuttavia, abbiamo anche informazioni estranee che non vogliamo fornire ai nostri
utenti. Forse la tecnica che abbiamo usato nel Listato 9-13 non è la migliore da
usare in questo contesto: una chiamata a panic!
è più appropriata per un
problema di programmazione che per un problema di utilizzo, come discusso nel
Capitolo 9. Invece, utilizzeremo l’altra
tecnica che hai imparato nel Capitolo 9: restituire un
Result
che indica un successo o un errore.
Restituire un Result
Invece di Chiamare panic!
Possiamo invece ritornare un valore Result
che conterrà un’istanza di Config
nel caso di successo e descriverà il problema nel caso di errore. Cambieremo
anche il nome della funzione da new
a build
perché è buona pratica e molti
programmatori si aspettano che le funzioni new
non falliscano mai. Quando
Config::build
comunica con main
, possiamo usare il type Result
per
segnalare che si è verificato un problema. Possiamo quindi modificare main
per
convertire una variante Err
in un errore più pratico per i nostri utenti,
senza sporcare l’output con il testo su thread 'main'
e RUST_BACKTRACE
causata da una chiamata a panic!
.
Il Listato 12-9 mostra le modifiche che dobbiamo apportare al valore di ritorno
della funzione che stiamo chiamando Config::build
e al corpo della funzione
necessaria per ritornare un Result
. Nota che questa funzione non verrà
compilata finché non aggiorneremo anche main
, cosa che faremo nel prossimo
Listato.
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Cerco {}", config.query);
println!("Nel file {}", config.percorso_file);
let contenuto = fs::read_to_string(config.percorso_file)
.expect("Dovrebbe essere stato possibile leggere il file");
println!("Con il testo:\n{contenuto}");
}
struct Config {
query: String,
percorso_file: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("Non ci sono abbastanza argomenti");
}
let query = args[1].clone();
let percorso_file = args[2].clone();
Ok(Config { query, percorso_file })
}
}
Result
da Config::build
La nostra funzione build
ritorna un Result
con un’istanza di Config
in
caso di successo e un letterale stringa in caso di errore. I nostri valori di
errore saranno sempre letterali stringa con lifetime 'static
.
Abbiamo apportato due modifiche al corpo della funzione: invece di chiamare
panic!
quando l’utente non passa abbastanza argomenti, ora restituiamo un
valore Err
e abbiamo racchiuso il valore restituito da Config
in un Ok
.
Queste modifiche rendono la funzione conforme al nuovo type ritornato.
Ritornare un valore Err
da Config::build
consente alla funzione main
di
gestire il valore Result
ritornato dalla funzione build
e di uscire dal
processo in modo più pulito in caso di errore.
Chiamare Config::build
e Gestire gli Errori
Per gestire il caso di errore e visualizzare un messaggio intuitivo, dobbiamo
aggiornare main
per gestire il Result
ritornato da Config::build
, come
mostrato nel Listato 12-10. Ci assumeremo anche la responsabilità di terminare
il nostro strumento da riga di comando con un codice di errore diverso da zero
ma senza panic!
, e lo implementeremo manualmente. Uno stato di uscita diverso
da zero è una convenzione per segnalare al processo chiamante che il programma è
uscito con uno stato di errore.
use std::env;
use std::fs;
use std::process;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problema nella lettura degli argomenti: {err}");
process::exit(1);
});
// --taglio--
println!("Cerco {}", config.query);
println!("Nel file {}", config.percorso_file);
let contenuto = fs::read_to_string(config.percorso_file)
.expect("Dovrebbe essere stato possibile leggere il file");
println!("Con il testo:\n{contenuto}");
}
struct Config {
query: String,
percorso_file: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("Non ci sono abbastanza argomenti");
}
let query = args[1].clone();
let percorso_file = args[2].clone();
Ok(Config { query, percorso_file })
}
}
Config
In questo Listato, abbiamo utilizzato un metodo che non abbiamo ancora trattato
in dettaglio: unwrap_or_else
, definito su Result<T, E>
dalla libreria
standard. L’utilizzo di unwrap_or_else
ci consente di definire una gestione
degli errori personalizzata, a differenza di panic!
. Se Result
è un valore
Ok
, il comportamento di questo metodo è simile a unwrap
: restituisce il
valore interno che Ok
sta racchiudendo. Tuttavia, se il valore è un valore
Err
, questo metodo richiama il codice nella closure, che è una funzione
anonima che definiamo e passiamo come argomento a unwrap_or_else
. Tratteremo
le closure (chiusure) più in dettaglio nel Capitolo 13. Per ora, è sufficiente sapere che unwrap_or_else
passerà il valore
interno di Err
, che in questo caso è la stringa statica "Non ci sono abbastanza argomenti"
che abbiamo aggiunto nel Listato 12-9, alla nostra
chiusura nell’argomento err
che appare tra le barre verticali. Il codice nella
chiusura può quindi utilizzare il valore err
durante l’esecuzione.
Abbiamo aggiunto una nuova riga use
per portare process
dalla libreria
standard nello scope. Il codice nella chiusura che verrà eseguito in caso di
errore è composto da sole due righe: stampiamo il valore err
e poi chiamiamo
process::exit
. La funzione process::exit
interromperà immediatamente il
programma e ritornerà il numero che è stato passato come codice di stato di
uscita. Questo è simile alla gestione basata su panic!
che abbiamo usato nel
Listato 12-8, ma otteniamo un output più pulito. Proviamolo:
$ cargo run
Compiling minigrep v0.1.0 (file:///progetti/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/minigrep`
Problema nella lettura degli argomenti: Non ci sono abbastanza argomenti
Ottimo! Questo output è molto più intuitivo per i nostri utenti.
Estrarre la Logica da main
Ora che abbiamo completato il refactoring dell’analisi della configurazione,
passiamo alla logica del programma. Come scritto nel paragrafo “Separare le
Attività per i Progetti Binari”, estrarremo una funzione denominata esegui
che conterrà tutta la
logica attualmente presente nella funzione main
che non è coinvolta
nell’impostazione della configurazione o nella gestione degli errori. Al
termine, la funzione main
sarà concisa e facile da verificare tramite
ispezione, e saremo in grado di scrivere test per tutta la restante logica.
Il Listato 12-11 mostra il piccolo miglioramento incrementale dell’estrazione di
una funzione esegui
.
use std::env;
use std::fs;
use std::process;
fn main() {
// --taglio--
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problema nella lettura degli argomenti: {err}");
process::exit(1);
});
println!("Cerco {}", config.query);
println!("Nel file {}", config.percorso_file);
esegui(config);
}
fn esegui(config: Config) {
let contenuto = fs::read_to_string(config.percorso_file)
.expect("Dovrebbe essere stato possibile leggere il file");
println!("Con il testo:\n{contenuto}");
}
// --taglio--
struct Config {
query: String,
percorso_file: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("Non ci sono abbastanza argomenti");
}
let query = args[1].clone();
let percorso_file = args[2].clone();
Ok(Config { query, percorso_file })
}
}
esegui
contenente il resto della logica del programmaLa funzione esegui
ora contiene tutta la logica rimanente di main
, a partire
dalla lettura del file. La funzione esegui
prende l’istanza Config
come
argomento.
Restituire Errori dalla Funzione esegui
Con la logica del programma rimanente separata nella funzione esegui
, possiamo
migliorare la gestione degli errori, come abbiamo fatto con Config::build
nel
Listato 12-9. Invece di lasciare che il programma vada in panico chiamando
expect
, la funzione esegui
ritornerà Result<T, E>
quando qualcosa va
storto. Questo ci permetterà di consolidare ulteriormente la logica di gestione
degli errori in main
in modo intuitivo. Il Listato 12-12 mostra le modifiche
che dobbiamo apportare alla firma e al corpo di esegui
.
use std::env;
use std::fs;
use std::process;
use std::error::Error;
// --taglio--
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problema nella lettura degli argomenti: {err}");
process::exit(1);
});
println!("Cerco {}", config.query);
println!("Nel file {}", config.percorso_file);
esegui(config);
}
fn esegui(config: Config) -> Result<(), Box<dyn Error>> {
let contenuto = fs::read_to_string(config.percorso_file)?;
println!("Con il testo:\n{contenuto}");
Ok(())
}
struct Config {
query: String,
percorso_file: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("Non ci sono abbastanza argomenti");
}
let query = args[1].clone();
let percorso_file = args[2].clone();
Ok(Config { query, percorso_file })
}
}
esegui
per ritornare Result
Abbiamo apportato tre modifiche significative. Innanzitutto, abbiamo cambiato il
type di ritorno della funzione esegui
in Result<(), Box<dyn Error>>
.
Questa funzione in precedenza restituiva il type unitario, ()
, e lo
manteniamo come valore restituito nel caso Ok
.
Per il type di errore, abbiamo utilizzato l’oggetto trait Box<dyn Error>
(e abbiamo portato std::error::Error
nello scope con un’istruzione use
all’inizio). Tratteremo gli oggetti trait nel Capitolo 18. Per ora, sappi solo che Box<dyn Error>
significa che la funzione
restituirà un type che implementa il trait Error
, ma non dobbiamo
specificare di quale type specifico sarà il valore restituito. Questo ci offre
la flessibilità di restituire valori di errore che possono essere di type
diverso in diversi casi di errore. La parola chiave dyn
è l’abbreviazione di
dynamic.
In secondo luogo, abbiamo rimosso la chiamata a expect
a favore dell’operatore
?
, come abbiamo illustrato nel Capitolo 9.
Invece di panic!
in caso di errore, ?
ritornerà il valore di errore dalla
funzione corrente affinché il chiamante possa gestirlo.
In terzo luogo, la funzione esegui
ora ritorna un valore Ok
in caso di
successo. Abbiamo dichiarato il type di successo della funzione esegui
come
()
nella firma, il che significa che dobbiamo racchiudere il valore del type
unitario nel valore Ok
. Questa sintassi Ok(())
potrebbe sembrare un po’
strana a prima vista, ma usare ()
in questo modo è il modo idiomatico per
indicare che chiamando esegui
vogliamo solo gestirne i suoi effetti
collaterali; non deve restituire un valore di cui abbiamo bisogno.
Quando esegui questo codice, verrà compilato ma verrà visualizzato un avviso:
$ cargo run -- ciao poesia.txt
Compiling minigrep v0.1.0 (file:///progetti/minigrep)
warning: unused `Result` that must be used
--> src/main.rs:19:5
|
19 | esegui(config);
| ^^^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
19 | let _ = esegui(config);
| +++++++
warning: `minigrep` (bin "minigrep") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.71s
Running `target/debug/minigrep ciao poesia.txt`
Cerca ciao
Nel file poesia.txt
Con il testo:
Io sono Nessuno! Tu chi sei?
Sei Nessuno anche tu?
Allora siamo in due!
Non dirlo! Potrebbero spargere la voce!
Che grande peso essere Qualcuno!
Così volgare — come una rana
che gracida il tuo nome — tutto giugno —
ad un pantano in estasi di lei!
Rust ci dice che il nostro codice ha ignorato il valore Result
e il valore
Result
potrebbe indicare che si è verificato un errore. Ma non stiamo
verificando se si è verificato un errore e il compilatore ci ricorda che
probabilmente intendevamo inserire del codice di gestione degli errori!
Risolviamo subito il problema.
Gestire gli Errori Restituiti da esegui
in main
Verificheremo la presenza di errori e li gestiremo utilizzando una tecnica
simile a quella utilizzata con Config::build
nel Listato 12-10, ma con una
leggera differenza:
File: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;
fn main() {
// --taglio--
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problema nella lettura degli argomenti: {err}");
process::exit(1);
});
println!("Cerco {}", config.query);
println!("Nel file {}", config.percorso_file);
if let Err(e) = esegui(config) {
println!("Errore applicazione: {e}");
process::exit(1);
}
}
fn esegui(config: Config) -> Result<(), Box<dyn Error>> {
let contenuto = fs::read_to_string(config.percorso_file)?;
println!("Con il testo:\n{contenuto}");
Ok(())
}
struct Config {
query: String,
percorso_file: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("Non ci sono abbastanza argomenti");
}
let query = args[1].clone();
let percorso_file = args[2].clone();
Ok(Config { query, percorso_file })
}
}
Utilizziamo if let
anziché unwrap_or_else
per verificare se esegui
restituisce un valore Err
e per chiamare process::exit(1)
in tal caso. La
funzione esegui
non restituisce un valore di cui abbiamo bisogno come nel caso
di Config::build
che restituisce l’istanza di Config
. Poiché esegui
restituisce ()
in caso di successo, ci interessa solo rilevare un errore,
quindi non abbiamo bisogno di unwrap_or_else
per restituire il valore estratto
da Ok
, che sarebbe solo ()
.
I corpi delle funzioni if let
e unwrap_or_else
sono gli stessi in entrambi i
casi: stampiamo l’errore ed usciamo.
Suddividere il Codice in un Crate Libreria
Il nostro progetto minigrep
sembra funzionare bene finora! Ora suddivideremo
il file src/main.rs e inseriremo del codice nel file src/lib.rs. In questo
modo, possiamo testare il codice e avere un file src/main.rs con meno
responsabilità.
Definiamo il codice responsabile della ricerca del testo in src/lib.rs anziché
in src/main.rs, il che permetterà a noi (o a chiunque altro utilizzi la nostra
libreria minigrep
) di chiamare la funzione di ricerca da più contesti rispetto
al nostro binario minigrep
.
Per prima cosa, definiamo la firma della funzione cerca
in src/lib.rs come
mostrato nel Listato 12-13, con un corpo che richiama la macro unimplemented!
.
Spiegheremo la firma più dettagliatamente quando completeremo l’implementazione.
pub fn cerca<'a>(query: &str, contenuto: &'a str) -> Vec<&'a str> {
unimplemented!();
}
cerca
in src/lib.rsAbbiamo utilizzato la parola chiave pub
nella definizione della funzione per
designare cerca
come parte dell’API pubblica del nostro crate libreria. Ora
abbiamo un crate libreria che possiamo utilizzare dal nostro binario e che
possiamo testare!
Ora dobbiamo inserire il codice definito in src/lib.rs nello scope del contenitore binario in src/main.rs e chiamarlo, come mostrato nel Listato 12-14.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
// --taglio--
use minigrep::cerca;
fn main() {
// --taglio--
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problema nella lettura degli argomenti: {err}");
process::exit(1);
});
if let Err(e) = esegui(config) {
println!("Errore nell'applicazione: {e}");
process::exit(1);
}
}
// --taglio--
struct Config {
query: String,
percorso_file: String,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("Non ci sono abbastanza argomenti");
}
let query = args[1].clone();
let percorso_file = args[2].clone();
Ok(Config { query, percorso_file })
}
}
fn esegui(config: Config) -> Result<(), Box<dyn Error>> {
let contenuto = fs::read_to_string(config.percorso_file)?;
for line in cerca(&config.query, &contenuto) {
println!("{line}");
}
Ok(())
}
cerca
del crate libreria minigrep
in src/main.rsAggiungiamo una riga use minigrep::cerca
per portare la funzione cerca
dal
crate libreria nello scope del crate binario. Quindi, nella funzione
esegui
, anziché stampare il contenuto del file, chiamiamo la funzione cerca
e passiamo il valore config.query
e contenuto
come argomenti. Quindi,
esegui
utilizzerà un ciclo for
per stampare ogni riga restituita da cerca
che corrisponde alla query. Questo è anche un buon momento per rimuovere le
chiamate println!
nella funzione main
che visualizzava la query e il
percorso del file, in modo che il nostro programma stampi solo i risultati della
ricerca (se non si verificano errori).
Nota che la funzione di ricerca raccoglierà tutti i risultati in un vettore che ritornerà prima che venga stampato alcunché. Questa implementazione potrebbe essere lenta nel visualizzare i risultati quando si cercano file di grandi dimensioni, perché i risultati non vengono stampati man mano che vengono trovati; discuteremo un possibile modo per risolvere questo problema utilizzando gli iteratori nel Capitolo 13.
Wow! È stato un duro lavoro, ma ci siamo preparati per il successo futuro. Ora è molto più facile gestire gli errori e abbiamo reso il codice più modulare. Quasi tutto il nostro lavoro sarà svolto in src/lib.rs da ora in poi.
Sfruttiamo questa nuova modularità facendo qualcosa che sarebbe stato difficile con il vecchio codice, ma è facile con il nuovo: scriveremo dei test!