Refactoriser pour améliorer la modularité et la gestion des erreurs
Pour améliorer notre programme, nous allons corriger quatre problèmes liés à la structure du programme et à la façon dont il gère les erreurs potentielles. Premièrement, notre fonction main effectue désormais deux tâches : elle analyse les arguments et lit les fichiers. À mesure que notre programme grandit, le nombre de tâches distinctes que la fonction main gère augmentera. Quand une fonction accumule les responsabilités, elle devient plus difficile à comprendre, plus difficile à tester et plus difficile à modifier sans casser l’une de ses parties. Il est préférable de séparer les fonctionnalités de sorte que chaque fonction soit responsable d’une seule tâche.
Ce problème est aussi lié au deuxième : bien que query et file_path soient des variables de configuration de notre programme, des variables comme contents sont utilisées pour exécuter la logique du programme. Plus main s’allonge, plus nous aurons de variables à mettre en portée ; plus nous avons de variables en portée, plus il sera difficile de suivre l’objectif de chacune. Il est préférable de regrouper les variables de configuration dans une seule structure pour clarifier leur rôle.
Le troisième problème est que nous avons utilisé expect pour afficher un message d’erreur lorsque la lecture du fichier échoue, mais le message d’erreur affiche simplement Should have been able to read the file. La lecture d’un fichier peut échouer de nombreuses façons : par exemple, le fichier pourrait être manquant, ou nous pourrions ne pas avoir la permission de l’ouvrir. Actuellement, quelle que soit la situation, nous afficherions le même message d’erreur pour tout, ce qui ne donnerait aucune information à l’utilisateur !
Quatrièmement, nous utilisons expect pour gérer une erreur, et si l’utilisateur exécute notre programme sans spécifier suffisamment d’arguments, il obtiendra une erreur index out of bounds de Rust qui n’explique pas clairement le problème. Il serait préférable que tout le code de gestion des erreurs soit au même endroit, de sorte que les futurs mainteneurs n’aient qu’un seul endroit à consulter si la logique de gestion des erreurs devait changer. Avoir tout le code de gestion des erreurs au même endroit garantira également que nous affichons des messages significatifs pour nos utilisateurs finaux.
Résolvons ces quatre problèmes en refactorisant notre projet.
Séparer les responsabilités dans les projets binaires
Le problème organisationnel consistant à attribuer la responsabilité de plusieurs tâches à la fonction main est commun à de nombreux projets binaires. Par conséquent, de nombreux programmeurs Rust trouvent utile de séparer les différentes responsabilités d’un programme binaire lorsque la fonction main commence à devenir volumineuse. Ce processus comprend les étapes suivantes : - Diviser votre programme en un fichier main.rs et un fichier lib.rs, et déplacer la logique de votre programme dans lib.rs. - Tant que votre logique d’analyse de la ligne de commande est petite, elle peut rester dans la fonction main. - Lorsque la logique d’analyse de la ligne de commande commence à devenir complexe, l’extraire de la fonction main dans d’autres fonctions ou types.
- Diviser votre programme en un fichier main.rs et un fichier lib.rs, et déplacer la logique de votre programme dans lib.rs.
- Tant que votre logique d’analyse de la ligne de commande est petite, elle peut rester dans la fonction
main. - Lorsque la logique d’analyse de la ligne de commande commence à devenir complexe, l’extraire de la fonction
maindans d’autres fonctions ou types.
Les responsabilités qui restent dans la fonction main après ce processus devraient se limiter aux suivantes : - Appeler la logique d’analyse de la ligne de commande avec les valeurs des arguments - Mettre en place toute autre configuration - Appeler une fonction run dans lib.rs - Gérer l’erreur si run renvoie une erreur
- Appeler la logique d’analyse de la ligne de commande avec les valeurs des arguments
- Mettre en place toute autre configuration
- Appeler une fonction
rundans lib.rs - Gérer l’erreur si
runrenvoie une erreur
Ce patron consiste à séparer les responsabilités : main.rs gère l’exécution du programme et lib.rs gère toute la logique de la tâche en cours. Comme vous ne pouvez pas tester directement la fonction main, cette structure vous permet de tester toute la logique de votre programme en la déplaçant hors de la fonction main. Le code qui reste dans la fonction main sera suffisamment petit pour vérifier son exactitude en le lisant. Retravaillons notre programme en suivant ce processus.
Extraire l’analyseur d’arguments
Nous allons extraire la fonctionnalité d’analyse des arguments dans une fonction que main appellera. L’encart 12-5 montre le nouveau début de la fonction main qui appelle une nouvelle fonction parse_config, que nous définirons dans src/main.rs.
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let (query, file_path) = parse_config(&args);
// --snip--
println!("Searching for {query}");
println!("In file {file_path}");
let contents = fs::read_to_string(file_path)
.expect("Should have been able to read the file");
println!("With text:
{contents}");
}
fn parse_config(args: &[String]) -> (&str, &str) {
let query = &args[1];
let file_path = &args[2];
(query, file_path)
}
parse_config function from mainNous collectons toujours les arguments de ligne de commande dans un vecteur, mais au lieu d’assigner la valeur de l’argument à l’index 1 à la variable query et la valeur de l’argument à l’index 2 à la variable file_path dans la fonction main, nous passons le vecteur entier à la fonction parse_config. La fonction parse_config contient alors la logique qui détermine quel argument va dans quelle variable et renvoie les valeurs à main. Nous créons toujours les variables query et file_path dans main, mais main n’a plus la responsabilité de déterminer comment les arguments de ligne de commande et les variables correspondent.
Cette refonte peut sembler excessive pour notre petit programme, mais nous refactorisons par petites étapes incrémentales. Après avoir effectué ce changement, exécutez à nouveau le programme pour vérifier que l’analyse des arguments fonctionne toujours. Il est bon de vérifier vos progrès fréquemment, pour aider à identifier la cause des problèmes lorsqu’ils surviennent.
Regrouper les valeurs de configuration
Nous pouvons faire un autre petit pas pour améliorer davantage la fonction parse_config. Pour le moment, nous renvoyons un tuple, mais ensuite nous décomposons immédiatement ce tuple en parties individuelles. C’est un signé que nous n’avons peut-être pas encore la bonne abstraction.
Un autre indicateur qui montre qu’il y à une marge d’amélioration est la partie config de parse_config, qui implique que les deux valeurs que nous renvoyons sont liées et font toutes deux partie d’une seule valeur de configuration. Nous ne transmettons pas actuellement cette signification dans la structure des données autrement qu’en regroupant les deux valeurs dans un tuple ; nous allons plutôt placer les deux valeurs dans une structure et donner à chaque champ de la structure un nom significatif. Cela permettra aux futurs mainteneurs de ce code de comprendre plus facilement comment les différentes valeurs sont liées entre elles et quel est leur rôle.
L’encart 12-6 montre les améliorations apportées à la fonction parse_config.
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = parse_config(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
// --snip--
println!("With text:
{contents}");
}
struct Config {
query: String,
file_path: String,
}
fn parse_config(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
parse_config to return an instance of a Config structNous avons ajouté une structure nommée Config définie avec des champs nommés query et file_path. La signature de parse_config indique maintenant qu’elle renvoie une valeur Config. Dans le corps de parse_config, là où nous renvoyions auparavant des tranches de chaînes qui référençaient des valeurs String dans args, nous définissons maintenant Config pour contenir des valeurs String possédées. La variable args dans main est la propriétaire des valeurs d’arguments et ne fait que les prêter à la fonction parse_config, ce qui signifie que nous violerions les règles d’emprunt de Rust si Config essayait de prendre possession des valeurs dans args.
Il existe plusieurs façons de gérer les données String ; la plus simple, bien que quelque peu inefficace, est d’appeler la méthode clone sur les valeurs. Cela créera une copie complète des données que l’instance de Config pourra posséder, ce qui prend plus de temps et de mémoire que de stocker une référence aux données de la chaîne. Cependant, cloner les données rend aussi notre code très simple car nous n’avons pas à gérer les durées de vie des références ; dans ce contexte, sacrifier un peu de performance pour gagner en simplicité est un compromis qui en vaut la peine.
Les compromis de l’utilisation de clone
Il y à une tendance parmi les Rustacés à éviter d’utiliser clone pour résoudre les problèmes de possession en raison de son coût à l’exécution. Au chapitre 13, vous apprendrez à utiliser des méthodes plus efficaces dans ce type de situation. Mais pour l’instant, il convient de copier quelques chaînes de caractères pour continuer à faire des progrès, car vous ne ferez ces copies qu’une seule fois et que votre chemin de fichier et votre chaîne de requête sont très petits. Il vaut mieux avoir un programme qui fonctionne mais qui est un peu inefficace que d’essayer d’hyper-optimiser le code lors de votre première tentative. Avec plus d’expérience avec Rust, il sera plus facile de partir de la solution la plus efficace, mais pour l’instant, il est parfaitement acceptable d’appeler clone.
Nous avons mis à jour main de sorte qu’il place l’instance de Config renvoyée par parse_config dans une variable nommée config, et nous avons mis à jour le code qui utilisait auparavant les variables séparées query et file_path pour qu’il utilise désormais les champs de la structure Config.
Maintenant, notre code exprime plus clairement que query et file_path sont liés et que leur objectif est de configurer le fonctionnement du programme. Tout code qui utilise ces valeurs sait qu’il les trouvera dans l’instance config, dans les champs nommés selon leur fonction.
Créer un constructeur pour Config
Jusqu’ici, nous avons extrait la logique responsable de l’analyse des arguments de ligne de commande de main et l’avons placée dans la fonction parse_config. Cela nous a aidés à voir que les valeurs query et file_path étaient liées, et que cette relation devait être exprimée dans notre code. Nous avons ensuite ajouté une structure Config pour nommer la relation entre query et file_path et pouvoir renvoyer les noms des valeurs comme noms de champs de la structure depuis la fonction parse_config.
Maintenant que l’objectif de la fonction parse_config est de créer une instance de Config, nous pouvons transformer parse_config d’une simple fonction en une fonction nommée new associée à la structure Config. Ce changement rendra le code plus idiomatique. Nous pouvons créer des instances de types de la bibliothèque standard, comme String, en appelant String::new. De même, en transformant parse_config en une fonction new associée à Config, nous pourrons créer des instances de Config en appelant Config::new. L’encart 12-7 montre les changements que nous devons apporter.
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:
{contents}");
// --snip--
}
// --snip--
struct Config {
query: String,
file_path: String,
}
impl Config {
fn new(args: &[String]) -> Config {
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
}
parse_config into Config::newNous avons mis à jour main là où nous appelions parse_config pour appeler à la place Config::new. Nous avons changé le nom de parse_config en new et l’avons déplacée dans un bloc impl, ce qui associe la fonction new à Config. Essayez de compiler ce code à nouveau pour vous assurer qu’il fonctionne.
Corriger la gestion des erreurs
Nous allons maintenant travailler à la correction de notre gestion des erreurs. Rappelez-vous que tenter d’accéder aux valeurs du vecteur args à l’index 1 ou à l’index 2 provoquera un panic du programme si le vecteur contient moins de trois éléments. Essayez d’exécuter le programme sans aucun argument ; voici ce que cela donnera : console {{#include ../listings/ch12-an-io-project/listing-12-07/output.txt}}
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep`
thread 'main' panicked at src/main.rs:27:21:
index out of bounds: the len is 1 but the index is 1
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
La ligne index out of bounds: the len is 1 but the index is 1 est un message d’erreur destiné aux programmeurs. Il n’aidera pas nos utilisateurs finaux à comprendre ce qu’ils devraient faire à la place. Corrigeons cela maintenant.
Améliorer le message d’erreur
Dans l’encart 12-8, nous ajoutons une vérification dans la fonction new qui vérifiera que la tranche est suffisamment longue avant d’accéder aux index 1 et 2. Si la tranche n’est pas assez longue, le programme panique et affiche un meilleur message d’erreur.
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:
{contents}");
}
struct Config {
query: String,
file_path: String,
}
impl Config {
// --snip--
fn new(args: &[String]) -> Config {
if args.len() < 3 {
panic!("not enough arguments");
}
// --snip--
let query = args[1].clone();
let file_path = args[2].clone();
Config { query, file_path }
}
}
Ce code est similaire à la fonction Guess::new que nous avons écrite dans l’encart 9-13, où nous appelions panic! lorsque l’argument value était en dehors de la plage de valeurs valides. Au lieu de vérifier une plage de valeurs ici, nous vérifions que la longueur de args est d’au moins 3 et le reste de la fonction peut fonctionner en supposant que cette condition est remplie. Si args contient moins de trois éléments, cette condition sera true, et nous appelons la macro panic! pour mettre fin au programme immédiatement.
Avec ces quelques lignes de code supplémentaires dans new, exécutons à nouveau le programme sans aucun argument pour voir à quoi ressemble l’erreur maintenant : console {{#include ../listings/ch12-an-io-project/listing-12-08/output.txt}}
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep`
thread 'main' panicked at src/main.rs:26:13:
not enough arguments
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Cette sortie est meilleure : nous avons maintenant un message d’erreur raisonnable. Cependant, nous avons aussi des informations superflues que nous ne voulons pas donner à nos utilisateurs. La technique que nous avons utilisée dans l’encart 9-13 n’est peut-être pas la meilleure à utiliser ici : un appel à panic! est plus approprié pour un problème de programmation que pour un problème d’utilisation, [comme discuté au Chapitre 9][ch9-error-guidelines]. À la place, nous utiliserons l’autre technique que vous avez apprise au Chapitre 9 — [renvoyer un Result][ch9-result] qui indique soit un succès, soit une erreur.
Renvoyer un Result au lieu d’appeler panic!
Nous pouvons à la place renvoyer une valeur Result qui contiendra une instance de Config en cas de succès et décrira le problème en cas d’erreur. Nous allons aussi changer le nom de la fonction de new à build car de nombreux programmeurs s’attendent à ce que les fonctions new n’échouent jamais. Lorsque Config::build communique avec main, nous pouvons utiliser le type Result pour signaler qu’il y a eu un problème. Ensuite, nous pouvons modifier main pour convertir un variant Err en une erreur plus pratique pour nos utilisateurs, sans le texte environnant sur thread 'main' et RUST_BACKTRACE qu’un appel à panic! provoque.
L’encart 12-9 montre les changements que nous devons apporter à la valeur de retour de la fonction que nous appelons maintenant Config::build et au corps de la fonction pour renvoyer un Result. Notez que cela ne compilera pas tant que nous n’aurons pas aussi mis à jour main, ce que nous ferons dans le prochain encart.
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
let config = Config::new(&args);
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:
{contents}");
}
struct Config {
query: String,
file_path: String,
}
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();
Ok(Config { query, file_path })
}
}
Result from Config::buildNotre fonction build renvoie un Result avec une instance de Config en cas de succès et un littéral de chaîne en cas d’erreur. Nos valeurs d’erreur seront toujours des littéraux de chaîne qui ont la durée de vie 'static.
Nous avons apporté deux modifications au corps de la fonction : au lieu d’appeler panic! lorsque l’utilisateur ne passe pas assez d’arguments, nous renvoyons maintenant une valeur Err, et nous avons enveloppé la valeur de retour Config dans un Ok. Ces changements font que la fonction se conforme à sa nouvelle signature de type.
Renvoyer une valeur Err depuis Config::build permet à la fonction main de gérer la valeur Result renvoyée par la fonction build et de quitter le processus plus proprement en cas d’erreur.
Appeler Config::build et gérer les erreurs
Pour gérer le cas d’erreur et afficher un message convivial, nous devons mettre à jour main pour gérer le Result renvoyé par Config::build, comme illustré dans l’encart 12-10. Nous prendrons également la responsabilité de quitter l’outil en ligne de commande avec un code d’erreur non nul, en l’implémentant nous-mêmes au lieu de laisser panic! s’en charger. Un code de sortie non nul est une convention pour signaler au processus qui a appelé notre programme que celui-ci s’est terminé dans un état d’erreur.
use std::env;
use std::fs;
use std::process;
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);
});
// --snip--
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:
{contents}");
}
struct Config {
query: String,
file_path: String,
}
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();
Ok(Config { query, file_path })
}
}
Config failsDans cet encart, nous avons utilisé une méthode que nous n’avons pas encore couverte en détail : unwrap_or_else, qui est définie sur Result<T, E> par la bibliothèque standard. Utiliser unwrap_or_else nous permet de définir une gestion d’erreur personnalisée, sans panic!. Si le Result est une valeur Ok, le comportement de cette méthode est similaire à unwrap : elle renvoie la valeur interne que Ok enveloppe. Cependant, si la valeur est un Err, cette méthode appelle le code dans la fermeture, qui est une fonction anonyme que nous définissons et passons en argument à unwrap_or_else. Nous couvrirons les fermetures plus en détail dans le [Chapitre 13][ch13]. Pour l’instant, vous devez simplement savoir que unwrap_or_else passera la valeur interne de l’Err, qui dans ce cas est la chaîne statique "not enough arguments" que nous avons ajoutée dans l’encart 12-9, à notre fermeture dans l’argument err qui apparaît entre les barres verticales. Le code dans la fermeture peut alors utiliser la valeur err lorsqu’il s’exécute.
Nous avons ajouté une nouvelle ligne use pour importer process de la bibliothèque standard dans la portée. Le code dans la fermeture qui sera exécuté en cas d’erreur ne fait que deux lignes : nous affichons la valeur err puis appelons process::exit. La fonction process::exit arrêtera le programme immédiatement et renverra le nombre passé comme code de sortie. C’est similaire à la gestion basée sur panic! que nous avons utilisée dans l’encart 12-8, mais nous n’obtenons plus toute la sortie supplémentaire. Essayons : console {{#include ../listings/ch12-an-io-project/listing-12-10/output.txt}}
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/minigrep`
Problem parsing arguments: not enough arguments
Parfait ! Cette sortie est beaucoup plus conviviale pour nos utilisateurs.
Extraire la logique de main
Maintenant que nous avons fini de refactoriser l’analyse de la configuration, tournons-nous vers la logique du programme. Comme nous l’avons indiqué dans « Séparer les responsabilités dans les projets binaires », nous allons extraire une fonction nommée run qui contiendra toute la logique actuellement dans la fonction main qui n’est pas impliquée dans la mise en place de la configuration ou la gestion des erreurs. Une fois terminé, la fonction main sera concise et facile à vérifier par inspection, et nous pourrons écrire des tests pour toute la logique restante.
L’encart 12-11 montre la petite amélioration incrémentale consistant à extraire une fonction run.
use std::env;
use std::fs;
use std::process;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
run(config);
}
fn run(config: Config) {
let contents = fs::read_to_string(config.file_path)
.expect("Should have been able to read the file");
println!("With text:
{contents}");
}
// --snip--
struct Config {
query: String,
file_path: String,
}
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();
Ok(Config { query, file_path })
}
}
run function containing the rest of the program logicLa fonction run contient maintenant toute la logique restante de main, à partir de la lecture du fichier. La fonction run prend l’instance de Config comme argument.
Renvoyer les erreurs depuis run
Avec la logique restante du programme séparée dans la fonction run, nous pouvons améliorer la gestion des erreurs, comme nous l’avons fait avec Config::build dans l’encart 12-9. Au lieu de permettre au programme de paniquer en appelant expect, la fonction run renverra un Result<T, E> quand quelque chose se passe mal. Cela nous permettra de consolider davantage la logique de gestion des erreurs dans main de manière conviviale. L’encart 12-12 montre les changements que nous devons apporter à la signature et au corps de run.
use std::env;
use std::fs;
use std::process;
use std::error::Error;
// --snip--
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);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
run(config);
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
println!("With text:
{contents}");
Ok(())
}
struct Config {
query: String,
file_path: String,
}
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();
Ok(Config { query, file_path })
}
}
run function to return ResultNous avons apporté trois changements significatifs ici. Premièrement, nous avons changé le type de retour de la fonction run en Result<(), Box<dyn Error>>. Cette fonction renvoyait auparavant le type unité, (), et nous gardons cela comme valeur renvoyée dans le cas Ok.
Pour le type d’erreur, nous avons utilisé l’objet trait Box<dyn Error> (et nous avons importé std::error::Error dans la portée avec une instruction use en haut). Nous couvrirons les objets trait dans le [Chapitre 18][ch18]. Pour l’instant, sachez simplement que Box<dyn Error> signifie que la fonction renverra un type qui implémente le trait Error, mais nous n’avons pas à spécifier quel type particulier sera la valeur de retour. Cela nous donne la flexibilité de renvoyer des valeurs d’erreur qui peuvent être de types différents dans différents cas d’erreur. Le mot-clé dyn est l’abréviation de dynamic (dynamique).
Deuxièmement, nous avons supprimé l’appel à expect en faveur de l’opérateur ?, comme nous en avons parlé dans le [Chapitre 9][ch9-question-mark]. Au lieu de faire un panic! sur une erreur, ? renverra la valeur d’erreur depuis la fonction courante pour que l’appelant la gère.
Troisièmement, la fonction run renvoie maintenant une valeur Ok en cas de succès. Nous avons déclaré le type de succès de la fonction run comme () dans la signature, ce qui signifie que nous devons envelopper la valeur de type unité dans la valeur Ok. Cette syntaxe Ok(()) peut sembler un peu étrange au premier abord. Mais utiliser () de cette façon est la manière idiomatique d’indiquer que nous appelons run uniquement pour ses effets de bord ; elle ne renvoie pas de valeur dont nous avons besoin.
Lorsque vous exécuterez ce code, il compilera mais affichera un avertissement : console {{#include ../listings/ch12-an-io-project/listing-12-12/output.txt}}
$ cargo run -- the poem.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
warning: unused `Result` that must be used
--> src/main.rs:19:5
|
19 | run(config);
| ^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
help: use `let _ = ...` to ignore the resulting value
|
19 | let _ = run(config);
| +++++++
warning: `minigrep` (bin "minigrep") generated 1 warning
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.71s
Running `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.
How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!
Rust nous dit que notre code a ignoré la valeur Result et que la valeur Result pourrait indiquer qu’une erreur s’est produite. Mais nous ne vérifions pas s’il y a eu une erreur, et le compilateur nous rappelle que nous avions probablement l’intention d’avoir du code de gestion d’erreurs ici ! Rectifions ce problème maintenant.
Gérer les erreurs renvoyées par run dans main
Nous vérifierons les erreurs et les gérerons en utilisant une technique similaire à celle que nous avons utilisée avec Config::build dans l’encart 12-10, mais avec une légère différence :
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;
fn main() {
// --snip--
let args: Vec<String> = env::args().collect();
let config = Config::build(&args).unwrap_or_else(|err| {
println!("Problem parsing arguments: {err}");
process::exit(1);
});
println!("Searching for {}", config.query);
println!("In file {}", config.file_path);
if let Err(e) = run(config) {
println!("Application error: {e}");
process::exit(1);
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
println!("With text:
{contents}");
Ok(())
}
struct Config {
query: String,
file_path: String,
}
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();
Ok(Config { query, file_path })
}
}
Nous utilisons if let plutôt que unwrap_or_else pour vérifier si run renvoie une valeur Err et appeler process::exit(1) si c’est le cas. La fonction run ne renvoie pas de valeur que nous voulons « déballer » (unwrap) de la même manière que Config::build renvoie l’instance de Config. Comme run renvoie () en cas de succès, nous ne nous soucions que de détecter une erreur, nous n’avons donc pas besoin de unwrap_or_else pour renvoyer la valeur déballée, qui ne serait que ().
Les corps de if let et des fonctions unwrap_or_else sont les mêmes dans les deux cas : nous affichons l’erreur et quittons.
Séparer le code dans un crate de bibliothèque
Notre projet minigrep a bonne allure jusqu’ici ! Maintenant, nous allons diviser le fichier src/main.rs et mettre du code dans le fichier src/lib.rs. De cette façon, nous pourrons tester le code et avoir un fichier src/main.rs avec moins de responsabilités.
Définissons le code responsable de la recherche de texte dans src/lib.rs plutôt que dans src/main.rs, ce qui nous permettra (ou permettra à quiconque utilise notre bibliothèque minigrep) d’appeler la fonction de recherche depuis plus de contextes que notre binaire minigrep.
Tout d’abord, définissons la signature de la fonction search dans src/lib.rs comme illustré dans l’encart 12-13, avec un corps qui appelle la macro unimplemented!. Nous expliquerons la signature plus en détail lorsque nous remplirons l’implémentation.
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
unimplemented!();
}
search function in src/lib.rsNous avons utilisé le mot-clé pub sur la définition de la fonction pour désigner search comme faisant partie de l’API publique de notre crate de bibliothèque. Nous avons maintenant un crate de bibliothèque que nous pouvons utiliser depuis notre crate binaire et que nous pouvons tester !
Nous devons maintenant importer le code défini dans src/lib.rs dans la portée du crate binaire dans src/main.rs et l’appeler, comme illustré dans l’encart 12-14.
use std::env;
use std::error::Error;
use std::fs;
use std::process;
// --snip--
use minigrep::search;
fn main() {
// --snip--
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);
}
}
// --snip--
struct Config {
query: String,
file_path: String,
}
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();
Ok(Config { query, file_path })
}
}
fn run(config: Config) -> Result<(), Box<dyn Error>> {
let contents = fs::read_to_string(config.file_path)?;
for line in search(&config.query, &contents) {
println!("{line}");
}
Ok(())
}
minigrep library crate’s search function in src/main.rsNous ajoutons une ligne use minigrep::search pour importer la fonction search du crate de bibliothèque dans la portée du crate binaire. Ensuite, dans la fonction run, au lieu d’afficher le contenu du fichier, nous appelons la fonction search et passons la valeur config.query et contents comme arguments. Ensuite, run utilisera une boucle for pour afficher chaque ligne renvoyée par search qui correspondait à la requête. C’est aussi le bon moment pour supprimer les appels à println! dans la fonction main qui affichaient la requête et le chemin du fichier, afin que notre programme n’affiche que les résultats de la recherche (si aucune erreur ne survient).
Notez que la fonction de recherche collectera tous les résultats dans un vecteur qu’elle renvoie avant que l’affichage n’ait lieu. Cette implémentation pourrait être lente à afficher les résultats lors de la recherche dans de gros fichiers, car les résultats ne sont pas affichés au fur et à mesure qu’ils sont trouvés ; nous discuterons d’une façon possible de corriger cela en utilisant les itérateurs au Chapitre 13.
Ouf ! C’était beaucoup de travail, mais nous nous sommes préparés pour le succès futur. Maintenant, il est beaucoup plus facile de gérer les erreurs, et nous avons rendu le code plus modulaire. Presque tout notre travail se fera dans src/lib.rs à partir de maintenant.
Profitons de cette nouvelle modularité en faisant quelque chose qui aurait été difficile avec l’ancien code mais qui est facile avec le nouveau : nous allons écrire des tests !