Shopping cart

0

Cart

  • 0 item

Nessun prodotto nel carrello.

All categories

SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI

Migliorare il nostro Progetto I/O

Con questa nuova conoscenza sugli iteratori, possiamo migliorare il progetto I/O nel Capitolo 12 utilizzando iteratori per rendere più chiari e concisi i luoghi nel codice. Vediamo come gli iteratori possono migliorare la nostra implementazione della funzione Config::build e della funzione search.

Rimozione di un clone utilizzando un Iteratore

In Lista 12-6, abbiamo aggiunto del codice che prendeva una slice di valori String e creava un’istanza della struttura Config indicizzando la slice e clonando i valori, consentendo alla struttura Config di possedere quei valori. In Lista 13-17, abbiamo riprodotto l’implementazione della funzione Config::build come era in Lista 12-23:

rust

impl Config {
pub 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 file_path = args[2].clone(); let ignore_case = env::var("IGNORE_CASE").is_ok();

Ok(Config {
query,
file_path,
ignore_case,
})
}
}

All’epoca, avevamo detto di non preoccuparci delle clonazioni inefficienti perché le avremmo rimosse in futuro. Bene, quel momento è ora!

Abbiamo dovuto clonare qui perché abbiamo una slice con elementi String nei parametri args, ma la funzione build non possiede args. Per restituire la proprietà di un’istanza di Config, abbiamo dovuto clonare i valori dai campi query e file_path di Config in modo che l’istanza di Config possa possedere i suoi valori.

Con la nostra nuova conoscenza sugli iteratori, possiamo modificare la funzione build per prendere la proprietà di un iteratore come suo argomento invece di prendere in prestito una slice. Utilizzeremo la funzionalità dell’iteratore invece del codice che controlla la lunghezza della slice e indica posizioni specifiche. Questo chiarirà cosa sta facendo la funzione Config::build perché l’iteratore accederà ai valori.

Una volta che Config::build prende la proprietà dell’iteratore e smette di usare operazioni di indicizzazione che prendono in prestito, possiamo spostare i valori String dall’iteratore in Config anziché chiamare clone e creare una nuova allocazione.

Utilizzo Diretto dell’Iteratore Restituito

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

rust

fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Problema nel parsing degli argomenti: {err}");
process::exit(1);
});

// --snip--
}

Prima di tutto, cambieremo l’inizio della funzione main che avevamo nella Lista 12-24 al codice nella Lista 13-18, che questa volta utilizza un iteratore. Questo non compilerà finché non aggiorneremo anche Config::build.

rust

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

// --snip--
}

La funzione env::args restituisce un iteratore! Piuttosto che raccogliere i valori dell’iteratore in un vettore e quindi passare una slice a Config::build, ora stiamo passando direttamente la proprietà dell’iteratore restituito da env::args a Config::build.

Successivamente, dobbiamo aggiornare la definizione di Config::build. Nel file src/lib.rs del tuo progetto I/O, modifichiamo la firma di Config::build per assomigliare alla Lista 13-19. Questo non compilerà ancora perché dobbiamo aggiornare anche il corpo della funzione.

rust

impl Config {
pub fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
// --snip--

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

Abbiamo aggiornato la firma della funzione Config::build in modo che il parametro args abbia un tipo generico con i vincoli del trait impl Iterator<Item = String> anziché &[String]. Questo uso della sintassi impl Trait che abbiamo discusso nella sezione “Traits come Parametri” del Capitolo 10 significa che args può essere qualsiasi tipo che implementa il tipo Iterator e restituisce elementi String.

Poiché stiamo prendendo la proprietà di args e la modificheremo iterando su di essa, possiamo aggiungere la parola chiave mut nella specifica del parametro args per renderlo mutabile.

Utilizzo dei Metodi del Trait Iterator anziché l’Indicizzazione

Successivamente, correggeremo il corpo di Config::build. Poiché args implementa il trait Iterator, sappiamo che possiamo chiamare il metodo next su di esso! La Lista 13-20 aggiorna il codice dalla Lista 12-23 per utilizzare il metodo next:

rust

impl Config {
pub 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("Non è stata fornita una stringa di query"),
}; let file_path = match args.next() {
Some(arg) => arg,
None => return Err("Non è stata fornita una directory di file"),
}; let ignore_case = env::var("IGNORE_CASE").is_ok();

Ok(Config {
query,
file_path,
ignore_case,
})
}
}

Ricorda che il primo valore nel valore restituito di 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. In secondo luogo, chiamiamo next per ottenere il valore che vogliamo mettere nel campo query di Config. Se next restituisce un Some, usiamo un match per estrarre il valore. Se restituisce None, significa che non sono stati forniti abbastanza argomenti e restituiamo presto un valore Err. Facciamo la stessa cosa per il valore file_path.

Rendere il Codice più Chiaro con Gli Adattatori dell’Iteratore

Possiamo anche sfruttare gli iteratori nella funzione search nel nostro progetto I/O, che è riprodotta qui nella Lista 13-21 come era nella Lista 12-19:

rust

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
let mut results = Vec::new();
for line in contents.lines() {
if line.contains(query) {
results.push(line);
}
}

results
}

Possiamo scrivere questo codice in modo più conciso utilizzando i metodi degli adattatori dell’iteratore. Farlo ci permette anche di evitare di avere un vettore di risultati intermedio mutabile. Lo stile di programmazione funzionale preferisce minimizzare la quantità di stato mutabile per rendere il codice più chiaro. Rimuovere lo stato mutabile potrebbe consentire un futuro miglioramento per effettuare la ricerca in modo parallelo, perché non dovremmo gestire l’accesso concorrente al vettore dei risultati. La Lista 13-22 mostra questa modifica:

rust

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}

Ricorda che lo scopo della funzione search è restituire tutte le righe in contents che contengono la query. Similmente all’esempio di filter nella Lista 13-16, questo codice utilizza l’adattatore di filtro per mantenere 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 per utilizzare i metodi dell’iteratore anche nella funzione search_case_insensitive.

Scegliere tra Cicli o Iteratori

La domanda logica successiva è quale stile dovresti scegliere nel tuo codice e perché: l’implementazione originale nella Lista 13-21 o la versione che utilizza gli iteratori nella Lista 13-22. La maggior parte dei programmatori Rust preferisce utilizzare lo stile dell’iteratore. È un po’ più difficile da capire all’inizio, ma una volta presa confidenza con i vari adattatori dell’iteratore e ciò che fanno, gli iteratori possono essere più facili da comprendere. Invece di giocherellare con i vari pezzi di ciclo e costruire nuovi vettori, il codice si concentra sull’obiettivo ad alto livello del ciclo. Questo astrae via parte del codice comune in modo che sia più facile vedere i concetti unici a questo codice, come la condizione di filtraggio che ogni elemento nell’iteratore deve superare.

Ma le due implementazioni sono veramente equivalenti? L’assunzione intuitiva potrebbe essere che il ciclo più dettagliato sarà più veloce. Parliamo di prestazioni.

1 Comment

Leave a Reply

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *