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::new
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-24, nous avons
reproduit l'implémentation de la fonction Config::new
telle qu'elle était dans
l'encart 12-23 à la fin du chapitre 12 :
Fichier : src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub recherche: String,
pub nom_fichier: String,
pub sensible_casse: bool,
}
impl Config {
pub fn new(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 nom_fichier = args[2].clone();
let sensible_casse = env::var("MINIGREP_INSENSIBLE_CASSE").is_err();
Ok(Config {
recherche,
nom_fichier,
sensible_casse,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contenu = fs::read_to_string(config.nom_fichier)?;
let resultats = if config.sensible_casse {
rechercher(&config.recherche, &contenu)
} else {
rechercher_insensible_casse(&config.recherche, &contenu)
};
for ligne in resultats {
println!("{}", ligne);
}
Ok(())
}
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
}
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:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], rechercher(recherche, contenu));
}
#[test]
fn case_insensitive() {
let recherche = "rUsT";
let contenu = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
rechercher_insensible_casse(recherche, contenu)
);
}
}
À 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 new
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 nom_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::new
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::process;
use minigrep::Config;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&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) = minigrep::run(config) {
eprintln!("Erreur applicative : {}", e);
process::exit(1);
}
}
Nous allons changer le début de la fonction main
que nous avions dans l'encart
12-24 pour le code dans l'encart 13-25. Ceci ne compilera pas encore jusqu'à ce
que nous mettions également à jour Config::new
.
Fichier : src/main.rs
use std::env;
use std::process;
use minigrep::Config;
fn main() {
let config = Config::new(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) = minigrep::run(config) {
eprintln!("Erreur applicative : {}", e);
process::exit(1);
}
}
La 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::new
, nous passons maintenant la possession de l'itérateur de
env::args
directement à Config::new
.
Ensuite, nous devons mettre à jour la définition de Config::new
. Dans le
fichier src/lib.rs de votre projet d'entrée/sortie, modifions la signature de
Config::new
pour qu'elle ressemble à l'encart 13-26. Ceci ne compilera pas
encore car nous devons mettre à jour le corps de la fonction.
Fichier : src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub recherche: String,
pub nom_fichier: String,
pub sensible_casse: bool,
}
impl Config {
pub fn new(mut args: env::Args) -> 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 nom_fichier = args[2].clone();
let sensible_casse = env::var("MINIGREP_INSENSIBLE_CASSE").is_err();
Ok(Config {
recherche,
nom_fichier,
sensible_casse,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contenu = fs::read_to_string(config.nom_fichier)?;
let resultats = if config.sensible_casse {
rechercher(&config.recherche, &contenu)
} else {
rechercher_insensible_casse(&config.recherche, &contenu)
};
for ligne in resultats {
println!("{}", ligne);
}
Ok(())
}
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
}
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:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], rechercher(recherche, contenu));
}
#[test]
fn case_insensitive() {
let recherche = "rUsT";
let contenu = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
rechercher_insensible_casse(recherche, contenu)
);
}
}
La documentation de la bibliothèque standard de la fonction env::args
indique que le type de l'itérateur qu'elle renvoie est std::env::Args
. Nous
avons mis à jour la signature de la fonction Config::new
pour que le
paramètre args
ait le type std::env::Args
au lieu de &[String]
. Etant
donné que nous prenons possession de args
et que nous allons muter args
en itérant dessus, nous pouvons ajouter le mot-clé mut
dans la spécification
du paramètre args
pour le rendre mutable.
Utilisation des méthodes du trait Iterator
au lieu des indices
Corrigeons ensuite le corps de Config::new
. La documentation de la bibliothèque
standard explique aussi que std::env::Args
implémente le trait Iterator
, donc
nous savons que nous pouvons appeler la méthode next
dessus ! L'encart 13-27
met à jour le code de l'encart 12-23 afin d'utiliser la méthode next
:
Fichier : src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub recherche: String,
pub nom_fichier: String,
pub sensible_casse: bool,
}
impl Config {
pub fn new(mut args: env::Args) -> 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 sensible_casse = env::var("MINIGREP_INSENSIBLE_CASSE").is_err();
Ok(Config {
recherche,
nom_fichier,
sensible_casse,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contenu = fs::read_to_string(config.nom_fichier)?;
let resultats = if config.sensible_casse {
rechercher(&config.recherche, &contenu)
} else {
rechercher_insensible_casse(&config.recherche, &contenu)
};
for ligne in resultats {
println!("{}", ligne);
}
Ok(())
}
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
}
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:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], rechercher(recherche, contenu));
}
#[test]
fn case_insensitive() {
let recherche = "rUsT";
let contenu = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
rechercher_insensible_casse(recherche, contenu)
);
}
}
Rappelez-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 nom_fichier
.
Rendre le code plus clair 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-28,
telles qu'elle était dans l'encart 12-19 à la fin du chapitre 12 :
Fichier : src/lib.rs
use std::error::Error;
use std::fs;
pub struct Config {
pub recherche: String,
pub nom_fichier: String,
}
impl Config {
pub fn new(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 nom_fichier = args[2].clone();
Ok(Config { recherche, nom_fichier })
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contenu = fs::read_to_string(config.nom_fichier)?;
Ok(())
}
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));
}
}
Nous 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-29 montre ce changement :
Fichier : src/lib.rs
use std::env;
use std::error::Error;
use std::fs;
pub struct Config {
pub recherche: String,
pub nom_fichier: String,
pub sensible_casse: bool,
}
impl Config {
pub fn new(mut args: std::env::Args) -> 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 sensible_casse = env::var("MINIGREP_INSENSIBLE_CASSE").is_err();
Ok(Config {
recherche,
nom_fichier,
sensible_casse,
})
}
}
pub fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contenu = fs::read_to_string(config.nom_fichier)?;
let resultats = if config.sensible_casse {
rechercher(&config.recherche, &contenu)
} else {
rechercher_insensible_casse(&config.recherche, &contenu)
};
for ligne in resultats {
println!("{}", ligne);
}
Ok(())
}
pub fn rechercher<'a>(recherche: &str, contenu: &'a str) -> Vec<&'a str> {
contenu
.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:
safe, fast, productive.
Pick three.
Duct tape.";
assert_eq!(vec!["safe, fast, productive."], rechercher(recherche, contenu));
}
#[test]
fn case_insensitive() {
let recherche = "rUsT";
let contenu = "\
Rust:
safe, fast, productive.
Pick three.
Trust me.";
assert_eq!(
vec!["Rust:", "Trust me."],
rechercher_insensible_casse(recherche, contenu)
);
}
}
Souvenez-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-19, nous pouvons utiliser l'adaptateur filter
pour garder uniquement 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
.
Logiquement la question suivante est de savoir quel style utiliser dans votre propre code et pourquoi : l'implémentation originale de l'encart 13-28 ou la version utilisant l'itérateur dans l'encart 13-29. 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.