Améliorer notre projet d’E/S
Avec ces nouvelles connaissances sur les iterateurs, nous pouvons améliorer le projet d’E/S du chapitre 12 en utilisant des iterateurs pour rendre certaines parties du code plus claires et plus concises. Voyons comment les iterateurs peuvent améliorer notre implémentation de la fonction Config::build et de la fonction search.
Supprimer un clone en utilisant un iterateur
Dans l’encart 12-6, nous avons ajouté du code qui prenait une slice de valeurs String et creait une instance de la struct Config en indexant la slice et en clonant les valeurs, permettant à la struct Config de posséder ces valeurs. Dans l’encart 13-17, nous avons reproduit l’implémentation de la fonction Config::build telle qu’elle était dans l’encart 12-23.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
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,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
Config::build function from Listing 12-23A l’epoque, nous avions dit de ne pas s’inquieter des appels inefficaces a clone car nous les supprimerions à l’avenir. Eh bien, ce moment est arrive !
Nous avions besoin de clone ici car nous avons une slice avec des éléments String dans le paramètre args, mais la fonction build ne possède pas args. Pour retourner la possession d’une instance de Config, nous devions cloner les valeurs des champs query et file_path de Config afin que l’instance de Config puisse posséder ses valeurs.
Avec nos nouvelles connaissances sur les iterateurs, nous pouvons modifier la fonction build pour qu’elle prenne la possession d’un iterateur comme argument au lieu d’emprunter une slice. Nous utiliserons la fonctionnalité d’iterateur au lieu du code qui vérifie la longueur de la slice et indexe des emplacements spécifiques. Cela clarifiera ce que fait la fonction Config::build car l’iterateur accède aux valeurs.
Une fois que Config::build prend la possession de l’iterateur et cesse d’utiliser des opérations d’indexation qui empruntent, nous pouvons deplacer les valeurs String de l’iterateur dans Config plutot que d’appeler clone et de faire une nouvelle allocation.
Utiliser l’iterateur retourné directement
Ouvrez le fichier src/main.rs de votre projet d’E/S, qui devrait ressembler a ceci :
Fichier : src/main.rs rust {{#rustdoc_include ../listings/ch05-using-structs-to-structure-related-data/no-listing-03-associated-functions/src/main.rs:here}}
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
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,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
Nous allons d’abord modifier le début de la fonction main que nous avions dans l’encart 12-24 par le code de l’encart 13-18, qui cette fois utilise un iterateur. Cela ne compilera pas tant que nous n’aurons pas également mis à jour Config::build.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
// --snip--
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("not enough arguments");
}
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,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
env::args to Config::buildLa fonction env::args retourné un iterateur ! Plutot que de collecter les valeurs de l’iterateur dans un vecteur puis de passer une slice a Config::build, nous passons maintenant directement la possession de l’iterateur retourné par env::args a Config::build.
Ensuite, nous devons mettre à jour la définition de Config::build. Modifions la signature de Config::build pour qu’elle ressemble à l’encart 13-19. Cela ne compilera toujours pas, car nous devons mettre à jour le corps de la fonction.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: bool,
}
impl Config {
fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
// --snip--
if args.len() < 3 {
return Err("not enough arguments");
}
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,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
Config::build to expect an iteratorLa documentation de la bibliothèque standard pour la fonction env::args montre que le type de l’iterateur qu’elle retourné est std::env::Args, et ce type implémenté le trait Iterator et retourné des valeurs String.
section of Chapter 10 means that args can be any type that implements the Iterator trait and returns String items. –> Nous avons mis à jour la signature de la fonction Config::build pour que le paramètre args ait un type générique avec les contraintes de trait impl Iterator<Item = String> au lieu de &[String]. Cette utilisation de la syntaxe impl Trait que nous avons abordee dans la section [“Utiliser les traits comme paramètres”][impl-trait] du chapitre 10 signifie que args peut être n’importe quel type qui implémenté le trait Iterator et retourné des éléments String.
Parce que nous prenons la possession de args et que nous allons muter args en iterant dessus, nous pouvons ajouter le mot-clé mut dans la specification du paramètre args pour le rendre mutable.
Utiliser les methodes du trait Iterator
Ensuite, nous allons corriger le corps de Config::build. Parce que args implémenté le trait Iterator, nous savons que nous pouvons appeler la methode next dessus ! L’encart 13-20 met à jour le code de l’encart 12-23 pour utiliser la methode next.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{search, search_case_insensitive};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Application error: {e}");
process::exit(1);
}
}
pub struct Config {
pub query: String,
pub file_path: String,
pub ignore_case: 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("Didn't get a query string"),
};
let file_path = match args.next() {
Some(arg) => arg,
None => return Err("Didn't get a file path"),
};
let ignore_case = env::var("IGNORE_CASE").is_ok();
Ok(Config {
query,
file_path,
ignore_case,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
let results = if config.ignore_case {
search_case_insensitive(&config.query, &contents)
} else {
search(&config.query, &contents)
};
for line in results {
println!("{line}");
}
Ok(())
}
Config::build to use iterator methodsRappelez-vous que la première valeur dans la valeur de retour de env::args est le nom du programme. Nous voulons l’ignorer et passer à la valeur suivante, donc nous appelons d’abord next et ne faisons rien avec la valeur de retour. Ensuite, nous appelons next pour obtenir la valeur que nous voulons mettre dans le champ query de Config. Si next retourné Some, nous utilisons un match pour extraire la valeur. Si elle retourné None, cela signifie que pas assez d’arguments ont été fournis, et nous retournons prematurement avec une valeur Err. Nous faisons la même chose pour la valeur file_path.
Clarifier le code avec les adaptateurs d’iterateurs
Nous pouvons également tirer parti des iterateurs dans la fonction search de notre projet d’E/S, qui est reproduite ici dans l’encart 13-21 telle qu’elle était dans l’encart 12-19.
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
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_result() {
let query = "duct";
let contents = "\n# Rust:
safe, fast, productive.
Pick three.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
}
search function from Listing 12-19Nous pouvons écrire ce code de maniere plus concise en utilisant des methodes d’adaptateur d’iterateur. Ce faisant, nous evitons également d’avoir un vecteur results intermediaire mutable. Le style de programmation fonctionnelle préfère minimiser la quantite d’état mutable pour rendre le code plus clair. Supprimer l’état mutable pourrait permettre une amélioration future pour effectuer la recherche en parallele, car nous n’aurions pas a gérer l’accès concurrent au vecteur results. L’encart 13-22 montre ce changement.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|line| line.contains(query))
.collect()
}
pub fn search_case_insensitive<'a>(
query: &str,
contents: &'a str,
) -> Vec<&'a str> {
let query = query.to_lowercase();
let mut results = Vec::new();
for line in contents.lines() {
if line.to_lowercase().contains(&query) {
results.push(line);
}
}
results
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn case_sensitive() {
let query = "duct";
let contents = "\n# Rust:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], search(query, contents));
}
#[test]
fn case_insensitive() {
let query = "rUsT";
let contents = "\n# Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
search_case_insensitive(query, contents)
);
}
}
search functionRappelez-vous que le but de la fonction search est de retourner toutes les lignes de contents qui contiennent la query. De maniere similaire à l’exemple filter de l’encart 13-16, ce code utilise l’adaptateur filter pour ne garder que les lignes pour lesquelles line.contains(query) retourné true. Nous collectons ensuite les lignes correspondantes dans un autre vecteur avec collect. Bien plus simple ! N’hesitez pas a faire le même changement pour utiliser les methodes d’iterateur dans la fonction search_case_insensitive également.
Pour une amélioration supplementaire, retournez un iterateur depuis la fonction search en supprimant l’appel a collect et en changeant le type de retour en impl Iterator<Item = &'a str> afin que la fonction devienne un adaptateur d’iterateur. Notez que vous devrez également mettre à jour les tests ! Effectuez une recherche dans un gros fichier en utilisant votre outil minigrep avant et après ce changement pour observer la différence de comportement. Avant ce changement, le programme n’affichera aucun résultat tant qu’il n’aura pas collecte tous les résultats, mais après le changement, les résultats seront affiches au fur et a mesure que chaque ligne correspondante est trouvee car la boucle for dans la fonction run peut tirer parti de la paresse de l’iterateur.
Choisir entre les boucles et les iterateurs
La question logique suivante est quel style vous devriez choisir dans votre propre code et pourquoi : l’implémentation originale de l’encart 13-21 ou la version utilisant les iterateurs de l’encart 13-22 (en supposant que nous collectons tous les résultats avant de les retourner plutot que de retourner l’iterateur). La plupart des programmeurs Rust preferent utiliser le style iterateur. C’est un peu plus difficile a maitriser au début, mais une fois que vous avez une bonne comprehension des différents adaptateurs d’iterateurs et de ce qu’ils font, les iterateurs peuvent être plus faciles a comprendre. Au lieu de bricoler les différentes parties des boucles et de construire de nouveaux vecteurs, le code se concentre sur l’objectif de haut niveau de la boucle. Cela abstrait une partie du code courant de sorte qu’il est plus facile de voir les concepts propres à ce code, comme la condition de filtrage que chaque élément de l’iterateur doit satisfaire.
Mais les deux implémentations sont-elles vraiment equivalentes ? L’hypothese intuitive pourrait être que la boucle de bas niveau sera plus rapide. Parlons des performances.