Amélioration de notre projet E/S

Beginner

This tutorial is from open-source community. Access the source code

Introduction

Bienvenue dans le projet Amélioration de notre E/S. Ce laboratoire est une partie du Livre Rust. Vous pouvez pratiquer vos compétences Rust dans LabEx.

Dans ce laboratoire, nous allons explorer comment les itérateurs peuvent être utilisés pour améliorer l'implémentation des fonctions Config::build et search dans le projet E/S du chapitre 12.

Amélioration de notre projet E/S

Avec ces nouvelles connaissances sur les itérateurs, nous pouvons améliorer le projet E/S du chapitre 12 en utilisant des itérateurs pour rendre les parties du code plus claires et plus concises. Regardons comment les itérateurs peuvent améliorer notre implémentation des fonctions Config::build et search.

Suppression d'un clonage à l'aide d'un itérateur

Dans la liste 12-6, nous avons ajouté du code qui prenait une tranche de valeurs de type String et créait une instance de la structure Config en utilisant l'indexation dans la tranche et en clonant les valeurs, permettant à la structure Config de posséder ces valeurs. Dans la liste 13-17, nous avons reproduit l'implémentation de la fonction Config::build telle qu'elle était dans la liste 12-23.

Nom de fichier : src/lib.rs

impl Config {
    pub 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,
        })
    }
}

Liste 13-17 : Reproduction de la fonction Config::build de la liste 12-23

À l'époque, nous avons dit de ne pas nous inquiéter des appels de clone inefficaces car nous les supprimerions plus tard. Eh bien, ce moment est maintenant!

Nous avons eu besoin de clone ici car nous avons une tranche avec des éléments de type 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 query et filename de Config afin que l'instance de Config puisse posséder ses valeurs.

Avec nos nouvelles connaissances sur les itérateurs, nous pouvons modifier la fonction build pour prendre la propriété d'un itérateur en tant qu'argument au lieu d'emprunter une tranche. Nous utiliserons les fonctionnalités de l'itérateur au lieu du code qui vérifie la longueur de la tranche et effectue des indexations dans des emplacements spécifiques. Cela clarifiera ce que fait la fonction Config::build car l'itérateur accédera aux valeurs.

Une fois que Config::build prend la propriété de l'itérateur et cesse d'utiliser des opérations d'indexation qui empruntent, nous pouvons déplacer les valeurs de type String de l'itérateur dans Config plutôt que d'appeler clone et de faire une nouvelle allocation.

Utiliser directement l'itérateur renvoyé

Ouvrez le fichier src/main.rs de votre projet E/S, qui devrait ressembler à ceci :

Nom de fichier : src/main.rs

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--
}

Nous allons tout d'abord modifier le début de la fonction main que nous avions dans la liste 12-24 pour le code de la liste 13-18, qui utilise cette fois un itérateur. Cela ne compilera pas tant que nous n'aurons pas mis à jour Config::build également.

Nom de fichier : src/main.rs

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

    --snip--
}

Liste 13-18 : Passer la valeur de retour de env::args à Config::build

La fonction env::args renvoie un itérateur! Au lieu de collecter les valeurs de l'itérateur dans un vecteur puis de passer une tranche à Config::build, nous passons maintenant la propriété de l'itérateur renvoyé par env::args directement à Config::build.

Ensuite, nous devons mettre à jour la définition de Config::build. Dans le fichier src/lib.rs de votre projet E/S, modifions la signature de Config::build pour qu'elle ressemble à la liste 13-19. Cela ne compilera toujours pas, car nous devons mettre à jour le corps de la fonction.

Nom de fichier : src/lib.rs

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

Liste 13-19 : Mettre à jour la signature de Config::build pour attendre un itérateur

La 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 la trait Iterator et renvoie des valeurs de type String.

Nous avons mis à jour la signature de la fonction Config::build de sorte 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 discutée dans "Traits en tant que paramètres" signifie que args peut être n'importe quel type qui implémente le type Iterator et renvoie des éléments de type String.

Comme nous prenons la propriété de args et que nous allons modifier args en itérant dessus, nous pouvons ajouter le mot clé mut dans la spécification du paramètre args pour le rendre mutable.

Utiliser des méthodes du trait Itérateur au lieu d'indexation

Ensuite, nous allons corriger le corps de Config::build. Puisque args implémente le trait Iterator, nous savons que nous pouvons appeler la méthode next dessus! La liste 13-20 met à jour le code de la liste 12-23 pour utiliser la méthode next.

Nom de fichier : src/lib.rs

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("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,
        })
    }
}

Liste 13-20 : Changement du corps de Config::build pour utiliser des méthodes d'itérateur

Rappelez-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 tout d'abord nous appelons 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 renvoie Some, nous utilisons un match pour extraire la valeur. Si elle renvoie None, cela signifie qu'il n'y a pas eu assez d'arguments et nous retournons rapidement avec une valeur Err. Nous faisons la même chose pour la valeur filename.

Rendre le code plus clair avec des adaptateurs d'itérateurs

Nous pouvons également tirer parti des itérateurs dans la fonction search de notre projet E/S. Elle est reproduite ici dans la liste 13-21 telle qu'elle était dans la liste 12-19.

Nom de fichier : src/lib.rs

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
}

Liste 13-21 : L'implémentation de la fonction search de la liste 12-19

Nous pouvons écrire ce code d'une manière plus concise en utilisant des méthodes d'adaptateurs d'itérateurs. Cela nous permet également d'éviter d'avoir un vecteur intermédiaire mutable results. Le style de programmation fonctionnelle préfère minimiser la quantité d'état mutable pour rendre le code plus clair. Supprimer l'état mutable pourrait permettre une amélioration future pour effectuer la recherche en parallèle car nous n'aurions pas à gérer l'accès concurrent au vecteur results. La liste 13-22 montre ce changement.

Nom de fichier : src/lib.rs

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

Liste 13-22 : Utilisation de méthodes d'adaptateurs d'itérateurs dans l'implémentation de la fonction search

Rappelez-vous que le but de la fonction search est de renvoyer toutes les lignes de contents qui contiennent la query. De manière similaire à l'exemple de filter dans la liste 13-16, ce code utilise l'adaptateur filter pour ne conserver que les lignes pour lesquelles line.contains(query) renvoie true. Nous collectons ensuite les lignes correspondantes dans un autre vecteur avec collect. Beaucoup plus simple! N'hésitez pas à apporter le même changement pour utiliser des méthodes d'itérateurs dans la fonction search_case_insensitive également.

Choix entre les boucles et les itérateurs

La prochaine question logique est laquelle des deux styles choisir dans votre propre code et pourquoi : l'implémentation initiale dans la liste 13-21 ou la version utilisant des itérateurs dans la liste 13-22. La plupart des programmeurs Rust préfèrent utiliser le style itérateur. C'est un peu plus difficile à maîtriser au départ, mais une fois que vous avez compris les différents adaptateurs d'itérateurs et ce qu'ils font, les itérateurs peuvent être plus faciles à comprendre. Au lieu de manipuler les différents éléments de la boucle et de construire de nouveaux vecteurs, le code se concentre sur l'objectif général de la boucle. Cela abstrait certains morceaux de code courants, de sorte qu'il est plus facile de voir les concepts qui sont uniques à ce code, comme la condition de filtrage que chaque élément de l'itérateur doit passer.

Mais les deux implémentations sont-elles vraiment équivalentes? L'hypothèse intuitive pourrait être que la boucle de bas niveau sera plus rapide. Parlons de performance.

Sommaire

Félicitations! Vous avez terminé le laboratoire d'amélioration de notre projet E/S. Vous pouvez pratiquer d'autres laboratoires dans LabEx pour améliorer vos compétences.