Amélioration de notre projet d’entrée/sortie
Grâce à ces nouvelles connaissances sur les itérateurs, nous pouvons améliorer le projet d’entrée/sortie du chapitre 12 en utilisant des itérateurs pour rendre certains endroits du code plus clairs et plus concis. Voyons comment les itérateurs peuvent améliorer notre implémentation de la fonction Config::build et de la fonction rechercher.
Supprimer l’appel à clone à l’aide d’un itérateur
Dans l’encart 12-6, nous avions ajouté du code qui prenait une slice de String et qui créait une instance de la structure Config en utilisant les indices de la slice et en clonant les valeurs, permettant ainsi à la structure 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 à la fin du chapitre 12 :
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{rechercher, rechercher_insensible_casse};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problème rencontré lors de l'interprétation des arguments : {err}");
process::exit(1);
});
if let Err(e) = run(config) {
println!("Erreur applicative : {e}");
process::exit(1);
}
}
pub struct Config {
pub recherche: String,
pub chemin_fichier: String
pub ignore_casse: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("il n'y a pas assez d'arguments");
}
let recherche = args[1].clone();
let chemin_fichier = args[2].clone();
let ignore_casse = env::var("IGNORE_CASSE").is_ok();
Ok(Config {
recherche,
chemin_fichier,
ignore_casse,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contenu = fs::read_to_string(config.chemin_fichier)?;
let resultats = if config.sensible_casse {
rechercher_insensible_casse(&config.recherche, &contenu)
} else {
recherche(&config.recherche, &contenu)
};
for ligne in resultats {
println!("{ligne}");
}
Ok(())
}
Config::build function from Listing 12-23À ce moment-là, nous avions dit de ne pas s’inquiéter des appels inefficaces à clone parce que nous les supprimerions à l’avenir. Et bien, ce moment est venu !
Nous avions besoin de clone ici parce que nous avons une slice d’éléments String dans le paramètre args, mais la fonction build ne possède pas args. Pour renvoyer la propriété d’une instance de Config, nous avons dû cloner les valeurs des champs recherche et chemin_fichier de Config afin que cette instance de Config puisse prendre possession de ces valeurs.
Avec nos nouvelles connaissances sur les itérateurs, nous pouvons changer la fonction new pour prendre possession d’un itérateur passé en argument au lieu d’emprunter une slice. Nous utiliserons les fonctionnalités des itérateurs à la place du code qui vérifie la taille de la slice et qui utilise les indices des éléments précis. Cela clarifiera ce que la fonction Config::new fait car c’est l’itérateur qui accédera aux valeurs.
Une fois que Config::build prend possession de l’itérateur et cesse d’utiliser les opérations avec les indices et d’emprunter les données, nous pouvons déplacer les valeurs String de l’iterator dans Config plutôt que de faire appel à clone et de créer par conséquent de nouvelles allocations.
Utiliser directement l’itérateur retourné
Ouvrez le fichier src/main.rs de votre projet d’entrée/sortie, qui devrait ressembler à ceci :
Fichier : src/main.rs
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{rechercher, rechercher_insensible_casse};
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Problème rencontré lors de l'interprétation des arguments : {err}");
process::exit(1);
});
// -- partie masquée ici --
if let Err(e) = run(config) {
eprintln!("Erreur applicative : {e}");
process::exit(1);
}
}
pub struct Config {
pub recherche: String,
pub chemin_fichier: String
pub ignore_casse: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("il n'y a pas assez d'arguments");
}
let recherche = args[1].clone();
let chemin_fichier = args[2].clone();
let ignore_casse = env::var("IGNORE_CASSE").is_ok();
Ok(Config {
recherche,
chemin_fichier,
ignore_casse,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contenu = fs::read_to_string(config.chemin_fichier)?;
let resultats = if config.sensible_casse {
rechercher_insensible_casse(&config.recherche, &contenu)
} else {
recherche(&config.recherche, &contenu)
};
for ligne in resultats {
println!("{ligne}");
}
Ok(())
}
Nous allons commencer par changer le début de la fonction main que nous avions dans l’encart 12-24 pour le code dans l’encart 13-18 qui, cette fois-ci, utilise un itérateur. Ceci ne compilera pas encore jusqu’à ce que nous mettions également à jour Config::build.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{rechercher, rechercher_insensible_casse};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problème rencontré lors de l'interprétation des arguments : {err}");
process::exit(1);
});
// -- partie masquée ici --
if let Err(e) = run(config) {
eprintln!("Erreur applicative : {e}");
process::exit(1);
}
}
pub struct Config {
pub recherche: String,
pub chemin_fichier: String
pub ignore_casse: bool,
}
impl Config {
fn build(args: &[String]) -> Result<Config, &'static str> {
if args.len() < 3 {
return Err("il n'y a pas assez d'arguments");
}
let recherche = args[1].clone();
let chemin_fichier = args[2].clone();
let ignore_casse = env::var("IGNORE_CASSE").is_ok();
Ok(Config {
recherche,
chemin_fichier,
ignore_casse,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contenu = fs::read_to_string(config.chemin_fichier)?;
let resultats = if config.sensible_casse {
rechercher_insensible_casse(&config.recherche, &contenu)
} else {
recherche(&config.recherche, &contenu)
};
for ligne in resultats {
println!("{ligne}");
}
Ok(())
}
env::args to Config::buildLa fonction env::args retourne un itérateur ! Plutôt que de collecter les valeurs de l’itérateur dans un vecteur et de passer ensuite une slice à Config::build, nous passons maintenant la possession de l’itérateur de env::args directement à 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-16. Ceci ne compilera pas encore 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::{rechercher, rechercher_insensible_casse};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problème rencontré lors de l'interprétation des arguments : {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Erreur applicative : {e}");
process::exit(1);
}
}
pub struct Config {
pub recherche: String,
pub chemin_fichier: String
pub ignore_casse: bool,
}
impl Config {
fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
// -- partie masquée ici --
if args.len() < 3 {
return Err("il n'y a pas assez d'arguments");
}
let recherche = args[1].clone();
let chemin_fichier = args[2].clone();
let ignore_casse = env::var("IGNORE_CASSE").is_ok();
Ok(Config {
recherche,
chemin_fichier,
ignore_casse,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contenu = fs::read_to_string(config.chemin_fichier)?;
let resultats = if config.sensible_casse {
rechercher_insensible_casse(&config.recherche, &contenu)
} else {
recherche(&config.recherche, &contenu)
};
for ligne in resultats {
println!("{ligne}");
}
Ok(())
}
Config::build to expect an iteratorLa documentation de la bibliothèque standard pour la fonction env::args indique que le type de l’itérateur qu’elle renvoie est std::env::Args, et que ce type implémente le trait Iterator et renvoie des valeurs de type String.“
Nous avons mis à jour la signature de la fonction Config::build de manière à ce 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, dont nous avons parlé dans la section “Utilisation des traits comme paramètres” du chapitre 10, signifie que args peut être de n’importe quel type qui implémente le trait Iterator et renvoie des éléments de type String.
Comme nous prenons possession de args et que nous allons le modifierargs en le parcourant, nous pouvons ajouter le mot-clé mut dans la spécification du paramètre args pour le rendre modifiable.
Utilisation des méthodes du trait Iterator
Corrigeons ensuite le corps de Config::build. Comme args implémente le trait Iterator, nous savons que nous pouvons appeler la méthode next dessus ! L’encart 13-20 met à jour le code de l’encart 12-23 afin d’utiliser la méthode next :
use std::env;
use std::error::Error;
use std::fs;
use std::process;
use minigrep::{rechercher, rechercher_insensible_casse};
fn main() {
let config = Config::build(env::args()).unwrap_or_else(|err| {
eprintln!("Problème rencontré lors de l'interprétation des arguments : {err}");
process::exit(1);
});
if let Err(e) = run(config) {
eprintln!("Erreur applicative : {e}");
process::exit(1);
}
}
pub struct Config {
pub recherche: String,
pub chemin_fichier: String
pub ignore_casse: bool,
}
impl Config {
fn build(
mut args: impl Iterator<Item = String>,
) -> Result<Config, &'static str> {
args.next();
let recherche = match args.next() {
Some(arg) => arg,
None => return Err("nous n'avons pas de chaîne de caractères"),
};
let nom_fichier = match args.next() {
Some(arg) => arg,
None => return Err("nous n'avons pas de nom de fichier"),
};
let ignore_casse = env::var("IGNORE_CASSE").is_ok();
Ok(Config {
recherche,
chemin_fichier,
ignore_casse,
})
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contenu = fs::read_to_string(config.chemin_fichier)?;
let resultats = if config.sensible_casse {
rechercher_insensible_casse(&config.recherche, &contenu)
} else {
recherche(&config.recherche, &contenu)
};
for ligne in resultats {
println!("{ligne}");
}
Ok(())
}
Config::build to use iterator methodsRappelez-vous que la première valeur de ce qui est retourné par env::args est le nom du programme. Nous voulons ignorer cette valeur et passer à la suivante, donc d’abord nous appelons une fois next et nous ne faisons rien avec sa valeur de retour. Ensuite, nous appelons next pour obtenir la valeur que nous voulons mettre dans le champ recherche de Config. Si next renvoie un Some, nous utilisons un match pour extraire sa valeur. S’il retourne None, cela signifie que pas assez d’arguments ont été fournis, si bien que nous quittons aussitôt la fonction en retournant une valeur Err. Nous procédons de même pour la valeur chemin_fichier.
Clarification du code avec des adaptateurs d’itération
Nous pouvons également tirer parti des itérateurs dans la fonction rechercher de notre projet d’entrée/sortie, qui est reproduite ici dans l’encart 13-21, telle qu’elle était dans l’encart 12-19.
pub fn rechercher<'a>(recherche: &str, contenu: &'a str) -> Vec<&'a str> {
let mut resultats = Vec::new();
for ligne in contenu.lines() {
if ligne.contains(recherche) {
resultats.push(ligne);
}
}
resultats
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn un_resultat() {
let recherche = "duct";
let contenu = "\
Rust:
sécurité, rapidité, productivité.
Obtenez les trois en même temps.";
assert_eq!(vec!["sécurité, rapidité, productivité."], rechercher(recherche, contenu));
}
}
search function from Listing 12-19Nous pouvons écrire ce code de façon plus concise en utilisant des méthodes des adaptateurs d’itération. Ce faisant, nous évitons ainsi d’avoir le vecteur mutable resultats. Le style de programmation fonctionnelle préfère minimiser la quantité d’états modifiables pour rendre le code plus clair. Supprimer l’état mutable pourrait nous aider à faire une amélioration future afin que la recherche se fasse en parallèle, car nous n’aurions pas à gérer l’accès concurrent au vecteur resultats. L’encart 13-22 montre ce changement.
pub fn rechercher<'a>(recherche: &str, contenu: &'a str) -> Vec<&'a str> {
contents
.lines()
.filter(|ligne| ligne.contains(recherche))
.collect()
}
pub fn rechercher_insensible_casse<'a>(
recherche: &str,
contenu: &'a str,
) -> Vec<&'a str> {
let recherche = recherche.to_lowercase();
let mut resultats = Vec::new();
for ligne in contenu.lines() {
if ligne.to_lowercase().contains(&recherche) {
resultats.push(ligne);
}
}
resultats
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sensible_casse() {
let recherche = "duct";
let contenu = "\
Rust:
sécurité, rapidité, productivité.
Obtenez les trois en même temps.
Duct tape.";
assert_eq!(vec!["sécurité, rapidité, productivité."], rechercher(recherche, contenu));
}
#[test]
fn insensible_casse() {
let recherche = "rUsT";
let contenu = "\
Rust:
sécurité, rapidité, productivité.
Obtenez les trois en même temps.
C'est pas rustique.";
assert_eq!(
vec!["Rust:", "C'est pas rustique."],
rechercher_insensible_casse(recherche, contenu)
);
}
}
search functionSouvenez-vous que le but de la fonction rechercher est de renvoyer toutes les lignes dans contenu qui contiennent recherche. Comme dans l’exemple de filter dans l’encart 13-16, ce code utilise l’adaptateur filter pour ne garder que les lignes pour lesquelles ligne.contains(recherche) renvoie true. Nous collectons ensuite les lignes correspondantes dans un autre vecteur avec collect. C’est bien plus simple ! N’hésitez pas à faire le même changement pour utiliser les méthodes d’itération dans la fonction rechercher_insensible_casse.
Pour encore améliorer le code, renvoyez un itérateur depuis la fonction search en supprimant l’appel à collect et en changeant le type de retour à impl Iterator<Item = &'a str>, de sorte que la fonction devienne un adaptateur d’itérateur. Notez que vous devrez aussi mettre à jour les tests ! Faites une recherche dans un fichier volumineux en utilisant votre outil minigrep avant et après avoir procédé à ces changements, afin d’observer la différence de comportement. Avant cette modification, le programme n’affichait aucun résultat tant qu’il n’avait pas collecté tous les résultats, mais après la modification, les résultats vont s’afficher à mesure que chaque ligne correspondant au motif est trouvée, car la boucle for dans la fonction run est capable de tirer parti du fait que l’itérateur fait des évaluations paresseuses.
Choix entre boucles et itérateurs
Logiquement, la question suivante est de savoir quel style utiliser dans votre propre code et pourquoi : l’implémentation originale de l’encart 13-21 ou la version utilisant l’itérateur dans l’encart 13-22 (en supposition que nous collections tous les résultats avant de les retourner, au lieu de retourner l’itérateur). La plupart des développeurs Rust préfèrent utiliser le style avec l’itérateur. C’est un peu plus difficile à comprendre au début, mais une fois que vous avez compris les différents adaptateurs d’itération et ce qu’ils font, les itérateurs peuvent devenir plus faciles à comprendre. Au lieu de jongler avec différentes boucles et de construire de nouveaux vecteurs, ce code se concentre sur l’objectif de haut niveau de la boucle. Cette abstraction permet d’éliminer une partie du code trivial, de sorte qu’il soit plus facile de dégager les concepts propres à ce code, comme le filtrage de chaque élément de l’itérateur qui est appliqué.
Mais ces deux implémentations sont-elles réellement équivalentes ? L’hypothèse intuitive pourrait être que la boucle de plus bas niveau sera plus rapide. Intéressons nous donc maintenant à leurs performances.