Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

File: 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)
}
Listato 12-5: Estrazione di una funzione 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.

File: src/main.rs
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 }
}
Listato 12-6: Refactoring di 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.

File: src/main.rs
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 }
    }
}
Listato 12-7: Modifica di 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.

File: src/main.rs
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 }
    }
}
Listato 12-8: Aggiunta di un controllo per il numero di argomenti

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.

File: src/main.rs
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 })
    }
}
Listato 12-9: Ritorno di un 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.

File: src/main.rs
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 })
    }
}
Listato 12-10: Terminazione con un codice di errore se fallisce la creazione di 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.

File: src/main.rs
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 })
    }
}
Listato 12-11: Estrazione di una funzione esegui contenente il resto della logica del programma

La 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.

File: src/main.rs
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 })
    }
}
Listato 12-12: Modifica della funzione 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.

File: src/lib.rs
pub fn cerca<'a>(query: &str, contenuto: &'a str) -> Vec<&'a str> {
    unimplemented!();
}
Listato 12-13: Definizione della funzione cerca in src/lib.rs

Abbiamo 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.

File: src/main.rs
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(())
}
Listato 12-14: Utilizzo della funzione cerca del crate libreria minigrep in src/main.rs

Aggiungiamo 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!