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

Migliorare il Nostro Progetto I/O

Con queste nuove conoscenze sugli iteratori, possiamo migliorare il progetto I/O del Capitolo 12 utilizzando gli iteratori per rendere alcuni punti del codice più chiari e concisi. Vediamo come gli iteratori possono migliorare l’implementazione della funzione Config::build e della funzione cerca.

Rimuovere clone Utilizzando un Iteratore

Nel Listato 12-6, abbiamo aggiunto del codice che prendeva una slice di valori String e creava un’istanza della struct Config indicizzando nella slice e clonando i valori, consentendo alla struct Config di avere ownership di tali valori. Nel Listato 13-17, abbiamo riprodotto l’implementazione della funzione Config::build così com’era nel Listato 12-23.

File: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{cerca, cerca_case_insensitive};

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);
    });

    if let Err(e) = esegui(config) {
        println!("Errore dell'applicazione: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub percorso_file: String,
    pub ignora_maiuscole: bool,
}

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();

        let ignora_maiuscole = env::var("IGNORA_MAIUSCOLE").is_ok();

        Ok(Config {
            query,
            percorso_file,
            ignora_maiuscole,
        })
    }
}

fn esegui(config: Config) -> Result<(), Box<dyn Error>> {
    let contenuto = fs::read_to_string(config.percorso_file)?;

    let risultato = if config.ignora_maiuscole {
        cerca_case_insensitive(&config.query, &contenuto)
    } else {
        cerca(&config.query, &contenuto)
    };

    for line in risultato {
        println!("{line}");
    }

    Ok(())
}
Listato 13-17: Riproduzione della funzione Config::build dal Listato 12-23

Allora, avevamo detto di non preoccuparci delle chiamate inefficienti a clone perché le avremmo rimosse in futuro. Bene, quel momento è arrivato!

Qui ci serviva clone perché abbiamo una slice con elementi String nel parametro args, ma la funzione build non ha ownership su args. Per restituire la ownership di un’istanza di Config, abbiamo dovuto clonare i valori dai campi query e percorso_file di Config in modo che l’istanza di Config possa possederne i valori.

Grazie alle nostre nuove conoscenze sugli iteratori, possiamo modificare la funzione build per prendere la ownership di un iteratore come argomento invece di prendere in prestito una slice. Utilizzeremo la funzionalità dell’iteratore invece del codice che controlla la lunghezza della slice e la indicizza in posizioni specifiche. Questo chiarirà cosa fa la funzione Config::build, perché l’iteratore accederà ai valori.

Una volta che Config::build assume la ownership dell’iteratore e smette di utilizzare le operazioni di indicizzazione che prendono in prestito, possiamo spostare i valori String dall’iteratore a Config anziché chiamare clone ed effettuare una nuova allocazione.

Utilizzare Direttamente l’Iteratore Restituito

Apri il file src/main.rs del tuo progetto I/O, che dovrebbe apparire così:

File: src/main.rs

use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{cerca, cerca_case_insensitive};

fn main() {
    let args: Vec<String> = env::args().collect();

    let config = Config::build(&args).unwrap_or_else(|err| {
        eprintln!("Problema nella lettura degli argomenti: {err}");
        process::exit(1);
    });

    // --taglio--

    if let Err(e) = esegui(config) {
        eprintln!("Errore dell'applicazione: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub percorso_file: String,
    pub ignora_maiuscole: bool,
}

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();

        let ignora_maiuscole = env::var("IGNORA_MAIUSCOLE").is_ok();

        Ok(Config {
            query,
            percorso_file,
            ignora_maiuscole,
        })
    }
}

fn esegui(config: Config) -> Result<(), Box<dyn Error>> {
    let contenuto = fs::read_to_string(config.percorso_file)?;

    let risultato = if config.ignora_maiuscole {
        cerca_case_insensitive(&config.query, &contenuto)
    } else {
        cerca(&config.query, &contenuto)
    };

    for line in risultato {
        println!("{line}");
    }

    Ok(())
}

Per prima cosa modifichiamo l’inizio della funzione main che avevamo nel Listato 12-24 con il codice nel Listato 13-18, che questa volta utilizza un iteratore. Questo non verrà compilato finché non aggiorneremo anche Config::build.

File: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{cerca, cerca_case_insensitive};

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Problema nella lettura degli argomenti: {err}");
        process::exit(1);
    });

    // --taglio--

    if let Err(e) = esegui(config) {
        eprintln!("Errore dell'applicazione: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub percorso_file: String,
    pub ignora_maiuscole: bool,
}

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();

        let ignora_maiuscole = env::var("IGNORA_MAIUSCOLE").is_ok();

        Ok(Config {
            query,
            percorso_file,
            ignora_maiuscole,
        })
    }
}

fn esegui(config: Config) -> Result<(), Box<dyn Error>> {
    let contenuto = fs::read_to_string(config.percorso_file)?;

    let risultato = if config.ignora_maiuscole {
        cerca_case_insensitive(&config.query, &contenuto)
    } else {
        cerca(&config.query, &contenuto)
    };

    for line in risultato {
        println!("{line}");
    }

    Ok(())
}
Listato 13-18: Passaggio del valore restituito da env::args a Config::build

La funzione env::args restituisce un iteratore! Invece di raccogliere i valori dell’iteratore in un vettore e poi passare una slice a Config::build, ora passiamo la ownership dell’iteratore restituito da env::args direttamente a Config::build.

Dobbiamo quindi aggiornare la definizione di Config::build. Modifichiamo la firma di Config::build in modo che assomigli al Listato 13-19. Questo non verrà comunque ancora compilato, perché dobbiamo aggiornare il corpo della funzione.

File: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{cerca, cerca_case_insensitive};

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Problema nella lettura degli argomenti: {err}");
        process::exit(1);
    });

    if let Err(e) = esegui(config) {
        eprintln!("Errore dell'applicazione: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub percorso_file: String,
    pub ignora_maiuscole: bool,
}

impl Config {
    fn build(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        // --taglio--
        if args.len() < 3 {
            return Err("non ci sono abbastanza argomenti");
        }

        let query = args[1].clone();
        let percorso_file = args[2].clone();

        let ignora_maiuscole = env::var("IGNORA_MAIUSCOLE").is_ok();

        Ok(Config {
            query,
            percorso_file,
            ignora_maiuscole,
        })
    }
}

fn esegui(config: Config) -> Result<(), Box<dyn Error>> {
    let contenuto = fs::read_to_string(config.percorso_file)?;

    let risultato = if config.ignora_maiuscole {
        cerca_case_insensitive(&config.query, &contenuto)
    } else {
        cerca(&config.query, &contenuto)
    };

    for line in risultato {
        println!("{line}");
    }

    Ok(())
}
Listato 13-19: Aggiornamento della firma di Config::build per aspettarsi un iteratore

La documentazione della libreria standard per la funzione env::args mostra che il tipo di iteratore restituito è std::env::Args, e che tale type implementa il trait Iterator e restituisce valori String.

Abbiamo aggiornato la firma della funzione Config::build in modo che il parametro args abbia un type generico con i vincoli del trait impl Iterator<Item = String> invece di &[String]. Questo utilizzo della sintassi impl Trait, discusso nella sezione “Usare i Trait come Parametri” del Capitolo 10, significa che args può essere qualsiasi type che implementi il trait Iterator e che restituisca elementi String.

Poiché stiamo prendendo la ownership di args e lo muteremo iterandolo, possiamo aggiungere la parola chiave mut nella specifica del parametro args per renderlo mutabile.

Utilizzare i Metodi del Trait Iterator

Successivamente, correggeremo il corpo di Config::build. Poiché args implementa il trait Iterator, sappiamo di poter chiamare il metodo next su di esso! Il Listato 13-20 aggiorna il codice del Listato 12-23 per utilizzare il metodo next.

File: src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;

use minigrep::{cerca, cerca_case_insensitive};

fn main() {
    let config = Config::build(env::args()).unwrap_or_else(|err| {
        eprintln!("Problema nella lettura degli argomenti: {err}");
        process::exit(1);
    });

    if let Err(e) = esegui(config) {
        eprintln!("Errore dell'applicazione: {e}");
        process::exit(1);
    }
}

pub struct Config {
    pub query: String,
    pub percorso_file: String,
    pub ignora_maiuscole: bool,
}

impl Config {
    fn build(
        mut args: impl Iterator<Item = String>,
    ) -> Result<Config, &'static str> {
        args.next();

        let query = match args.next() {
            Some(arg) => arg,
            None => return Err("Stringa Query non fornita"),
        };

        let percorso_file = match args.next() {
            Some(arg) => arg,
            None => return Err("Percorso file non fornito"),
        };

        let ignora_maiuscole = env::var("IGNORA_MAIUSCOLE").is_ok();

        Ok(Config {
            query,
            percorso_file,
            ignora_maiuscole,
        })
    }
}

fn esegui(config: Config) -> Result<(), Box<dyn Error>> {
    let contenuto = fs::read_to_string(config.percorso_file)?;

    let risultato = if config.ignora_maiuscole {
        cerca_case_insensitive(&config.query, &contenuto)
    } else {
        cerca(&config.query, &contenuto)
    };

    for line in risultato {
        println!("{line}");
    }

    Ok(())
}
Listato 13-20: Modifica del corpo di Config::build per utilizzare i metodi iteratori

Ricorda che il primo valore restituito da env::args è il nome del programma. Vogliamo ignorarlo e passare al valore successivo, quindi prima chiamiamo next e non facciamo nulla con il valore restituito. Poi chiamiamo next per ottenere il valore che vogliamo inserire nel campo query di Config. Se next restituisce Some, usiamo match per estrarre il valore. Se restituisce None, significa che non sono stati forniti abbastanza argomenti e restituiamo subito un valore Err. Facciamo la stessa cosa per il valore percorso_file.

Chiarire il Codice con gli Adattatori

Possiamo anche sfruttare gli iteratori nella funzione cerca nel nostro progetto di I/O, che è riprodotta qui nel Listato 13-21 come nel Listato 12-19.

File: src/lib.rs
pub fn cerca<'a>(query: &str, contenuto: &'a str) -> Vec<&'a str> {
    let mut risultato = Vec::new();

    for line in contenuto.lines() {
        if line.contains(query) {
            risultato.push(line);
        }
    }

    risultato
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn un_risultato() {
        let query = "dut";
        let contenuto = "\
Rust:
sicuro, veloce, produttivo.
Scegline tre.";

        assert_eq!(vec!["sicuro, veloce, produttivo."], cerca(query, contenuto));
    }
}
Listato 13-21: L’implementazione della funzione cerca del Listato 12-19

Possiamo scrivere questo codice in modo più conciso utilizzando gli adattatori. In questo modo evitiamo anche di avere un vettore mutabile risultato. Lo stile di programmazione funzionale preferisce ridurre al minimo la quantità di stato mutabile per rendere il codice più chiaro. La rimozione dello stato mutabile potrebbe consentire un miglioramento futuro per far sì che la ricerca avvenga in parallelo, poiché non dovremmo gestire l’accesso simultaneo al vettore risultati. Il Listato 13-22 mostra questa modifica.

File: src/lib.rs
pub fn cerca<'a>(query: &str, contenuto: &'a str) -> Vec<&'a str> {
    contenuto
        .lines()
        .filter(|line| line.contains(query))
        .collect()
}

pub fn cerca_case_insensitive<'a>(
    query: &str,
    contenuto: &'a str,
) -> Vec<&'a str> {
    let query = query.to_lowercase();
    let mut risultato = Vec::new();

    for line in contenuto.lines() {
        if line.to_lowercase().contains(&query) {
            risultato.push(line);
        }
    }

    risultato
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn case_sensitive() {
        let query = "dut";
        let contenuto = "\
Rust:
sicuro, veloce, produttivo.
Scegline tre.
Duttilità.";

        assert_eq!(vec!["sicuro, veloce, produttivo."], cerca(query, contenuto));
    }

    #[test]
    fn case_insensitive() {
        let query = "rUsT";
        let contenuto = "\
Rust:
sicuro, veloce, produttivo.
Scegline tre.
Una frusta.";

        assert_eq!(
            vec!["Rust:", "Una frusta."],
            cerca_case_insensitive(query, contenuto)
        );
    }
}
Listato 13-22: Utilizzo degli adattatori nell’implementazione della funzione cerca

Ricorda che lo scopo della funzione cerca è restituire tutte le righe in contenuto che contengono la query. Analogamente all’esempio filter nel Listato 13-16, questo codice utilizza l’adattatore filter per conservare solo le righe per le quali line.contains(query) restituisce true. Quindi raccogliamo le righe corrispondenti in un altro vettore con collect. Molto più semplice! Sentiti libero di apportare la stessa modifica utilizzando i metodi adattatori anche nella funzione cerca_case_insensitive.

Per un ulteriore miglioramento, restituisci un iteratore dalla funzione cerca rimuovendo la chiamata a collect e modificando il type di ritorno in impl Iterator<Item = &'a str> in modo che la funzione diventi essa stessa un adattatore. Nota che dovrai anche aggiornare i test! Prova ad utilizzare lo strumento minigrep per cercare in un file di grandi dimensioni prima e dopo aver apportato questa modifica ed osserva la differenza di comportamento. Prima di questa modifica, il programma non visualizzava alcun risultato finché non aveva raccolto tutti i risultati, ma dopo la modifica, i risultati verranno visualizzati man mano che viene trovata ogni riga corrispondente, perché il ciclo for nella funzione esegui è in grado di sfruttare “la pigrizia” (laziness) dell’iteratore.

Scegliere tra Cicli e Iteratori

La domanda logica successiva è quale stile scegliere nel proprio codice e perché: l’implementazione originale nel Listato 13-21 o la versione che utilizza gli iteratori nel Listato 13-22 (supponendo che stiamo raccogliendo tutti i risultati prima di restituirli piuttosto che restituire l’iteratore). La maggior parte dei programmatori Rust preferisce usare lo stile iteratore. È un po’ più difficile da capire all’inizio, ma una volta che si è presa familiarità con i vari adattatori e con il loro funzionamento, gli iteratori possono essere più facili da capire. Invece di armeggiare con i vari pezzi del ciclo e creare nuovi vettori, il codice si concentra sull’obiettivo di alto livello del ciclo. Questo astrae parte del codice più comune, rendendo più facile comprendere i concetti specifici di questo codice, come la condizione di filtro che ogni elemento dell’iteratore deve soddisfare.

Ma le due implementazioni sono davvero equivalenti? L’ipotesi intuitiva potrebbe essere che il ciclo di livello inferiore sia più veloce. Parliamo di prestazioni.