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.
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(())
}
Config::build
dal Listato 12-23Allora, 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
.
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(())
}
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.
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(())
}
Config::build
per aspettarsi un iteratoreLa 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
.
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(())
}
Config::build
per utilizzare i metodi iteratoriRicorda 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.
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));
}
}
cerca
del Listato 12-19Possiamo 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.
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)
);
}
}
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.