16. La gestion des erreurs▲
La gestion d'erreur est le processus de gestion d'un échec potentiel. Par exemple, ne pas parvenir à lire un fichier et continuer à utiliser malgré tout cette mauvaise entrée serait clairement problématique. Cerner et gérer explicitement ces erreurs épargne le reste du programme d'un bon nombre de problèmes.
Pour une explicitation plus exhaustive à propos de la gestion des erreurs, référez-vous à la section dédiée à la gestion des erreurs dans le livre officiel.
16-1. La macro panic▲
Le plus simple mécanisme de gestion d'erreur que nous allons voir est panic. Il affiche un message d'erreur, lance la tâche et généralement met fin au programme. Ici, nous appelons explicitement panic dans notre condition :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
fn
give_princess(gift: &
str) {
// Les princesses détestent les serpents, donc nous devons tout arrêter si elles n'acceptent pas !
if
gift == "snake"
{ panic!
("AAAaaaaa!!!!"
); }
println!
("I love
{}
s!!!!!"
, gift);
}
fn
main() {
give_princess("teddy bear"
);
give_princess("snake"
);
}
16-2. L'enum Option et la méthode unwrap▲
Dans le dernier exemple, nous avons montré qu'il était possible de mettre en échec le programme quand bon nous semble. Nous disions que notre programme pouvait "paniquer" si la princesse recevait un présent inapproprié - un serpent. Mais qu'en est-il du cas où la princesse attendait un cadeau mais n'en reçoit pas ? Ce cas de figure serait tout bonnement irrecevable, donc inutile d'être géré.
Nous pourrions tester ce cas contre une chaîne de caractères vide (""
), comme nous l'avons fait avec le serpent. Puisque nous utilisons Rust, laissons plutôt le compilateur gérer les cas où il n'y pas de cadeau.
Une enum
nommée Option
<T> dans la bibliothèque standard est utilisée lorsque "l'absence de" est une possibilité. Elle-même est représentée par deux variantes :
Some
(T) : Un élément de type T a été trouvé ;None
: Aucun élément n'a été trouvé.
Ces cas peuvent être explicitement gérés par le biais de match
ou implicitement avec unwrap. La gestion implicite renverra l'élément contenu en cas de succès, sinon un panic sera lancé.
Notez que s'il est possible de personnaliser manuellement le message d'erreur de panic, ce n'est pas le cas pour unwrap qui nous laissera avec des informations moins intelligibles qu'une gestion explicite. Dans l'exemple suivant, la gestion explicite offre un plus grand contrôle sur le résultat tout en permettant l'utilisation de panic, si désiré.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
// Le roturier a déjà tout vu et accepte de bon coeur n'importe quel présent.
// Tous les présents sont gérés explicitement en utilisant `match`.
fn
give_commoner(gift: Option
<&
str>) {
// On définit une action pour chaque cas.
match
gift {
Some
("snake"
) => println!
("Yuck! I'm throwing that snake in a fire."
),
Some
(inner) => println!
("
{}
? How nice."
, inner),
None
=> println!
("No gift? Oh well."
),
}
}
// Notre précieuse princesse va `panic` à la vue des serpents.
// Tous les présents sont gérés implicitement en utilisant `unwrap`.
fn
give_princess(gift: Option
<&
str>) {
// `unwrap` renvoie un `panic` lorsqu'il reçoit la variante `None`.
let
inside = gift.unwrap();
if
inside == "snake"
{ panic!
("AAAaaaaa!!!!"
); }
println!
("I love
{}
s!!!!!"
, inside);
}
fn
main() {
let
food = Some
("cabbage"
);
let
snake = Some
("snake"
);
let
void = None
;
give_commoner(food);
give_commoner(snake);
give_commoner(void);
let
bird = Some
("robin"
);
let
nothing = None
;
give_princess(bird);
give_princess(nothing);
}
16-2-1. Les combinateurs: map▲
match
est une approche valable pour gérer les Option
s. Cependant, vous pourriez trouver son utilisation fastidieuse, principalement avec des opérations qui ne sont valides qu'avec une entrée. Dans ces cas, les combinateurs peuvent être utilisés pour gérer le contrôle du flux de manière plus modulaire.
Option
possède une méthode préfaite appelée map(), un combinateur pour la simple mise en corrélation de Some
-> Some
et None
-> None
. Les appels de la méthode map() peuvent être chaînés ensemble pour plus de flexibilité.
Dans l'exemple suivant, process() remplace toutes les fonctions précédentes tout en restant compacte.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
#![allow(dead_code)]
#[derive(Debug)]
enum
Food { Apple, Carrot, Potato }
#[derive(Debug)]
struct
Peeled(Food);
#[derive(Debug)]
struct
Chopped(Food);
#[derive(Debug)]
struct
Cooked(Food);
// Épluchage de la nourriture. S'il n'y en a pas, alors on renvoie `None`.
// Sinon, on renvoie la nourriture épluchée.
fn
peel(food: Option
<Food>) -> Option
<Peeled> {
match
food {
Some
(food) => Some
(Peeled(food)),
None
=> None
,
}
}
// Éminçage de la nourriture. S'il n'y en a pas, alors on renvoie `None`.
// Sinon, on renvoie la nourriture émincée.
fn
chop(peeled: Option
<Peeled>) -> Option
<Chopped> {
match
peeled {
Some
(Peeled(food)) => Some
(Chopped(food)),
None
=> None
,
}
}
// Cuisson de la nourriture. Ici, nous utilisons `map()` au lieu de `match` pour la gestion des cas.
fn
cook(chopped: Option
<Chopped>) -> Option
<Cooked> {
chopped.map(|Chopped(food)| Cooked(food))
}
// Une fonction pour éplucher, émincer et cuire la nourriture dans une seule séquence.
// Nous chaînons plusieurs appels de `map()` pour simplifier le code.
fn
process(food: Option
<Food>) -> Option
<Cooked> {
food.map(|f| Peeled(f))
.map(|Peeled(f)| Chopped(f))
.map(|Chopped(f)| Cooked(f))
}
// On vérifie s'il y a de la nourritre ou pas avant d'essayer de la manger !
fn
eat(food: Option
<Cooked>) {
match
food {
Some
(food) => println!
("Mmm. I love
{:?}
"
, food),
None
=> println!
("Oh no! It wasn't edible."
),
}
}
fn
main() {
let
apple = Some
(Food::Apple);
let
carrot = Some
(Food::Carrot);
let
potato = None
;
let
cooked_apple = cook(chop(peel(apple)));
let
cooked_carrot = cook(chop(peel(carrot)));
// Vous remarquerez que `process()` est bien plus lisible.
let
cooked_potato = process(potato);
eat(cooked_apple);
eat(cooked_carrot);
eat(cooked_potato);
}
16-2-2. Les combinateurs: and_then▲
map() a été décrite comme étant un moyen de chaîner les directives pour simplifier les déclarations match
. Cependant, utiliser map() sur une fonction qui renvoie déjà une instance de Option
<T> risque d'imbriquer le résultat dans une autre instance Option
<Option
<T>>. Chaîner des appels peut alors prêter à confusion. C'est là où un autre combinateur nommé and_then(), connu dans d'autres langages sous le nom de flatmap, entre en jeu.
and_then() appelle la fonction passée en entrée avec la valeur imbriquée et renvoie le résultat. Si le conteneur Option
vaut None
, alors elle renvoie None
à la place.
Dans l'exemple suivant, cookable_v2() renvoie une instance de Option
<Food>. Utiliser la méthode map() au lieu de and_then() donnerait une instance imbriquée Option
<Option
<Food>>, qui est un type invalide pour la fonction eat().
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
#![allow(dead_code)]
#[derive(Debug)]
enum
Food { CordonBleu, Steak, Sushi }
#[derive(Debug)]
enum
Day { Monday, Tuesday, Wednesday }
// Nous n'avons pas les ingrédients pour faire des sushis.
fn
have_ingredients(food: Food) -> Option
<Food> {
match
food {
Food::Sushi => None
,
_ => Some
(food),
}
}
// Nous avons la recette de tous les mets sauf celle du Cordon Bleu.
fn
have_recipe(food: Food) -> Option
<Food> {
match
food {
Food::CordonBleu => None
,
_ => Some
(food),
}
}
// Pour faire un plat, nous avons besoin de deux ingrédients et d'une recette.
// Nous pouvons représenter la logique avec une chaîne de `match`s:
fn
cookable_v1(food: Food) -> Option
<Food> {
match
have_ingredients(food) {
None
=> None
,
Some
(food) => match
have_recipe(food) {
None
=> None
,
Some
(food) => Some
(food),
},
}
}
// On peut rendre plus compacte cette implémentation en utilisant `and_then()`:
fn
cookable_v2(food: Food) -> Option
<Food> {
have_ingredients(food).and_then(have_recipe)
}
fn
eat(food: Food, day: Day) {
match
cookable_v2(food) {
Some
(food) => println!
("Yay! On
{:?}
we get to eat
{:?}
."
, day, food),
None
=> println!
("Oh no. We don't get to eat on
{:?}
?"
, day),
}
}
fn
main() {
let
(cordon_bleu, steak, sushi) = (Food::CordonBleu, Food::Steak, Food::Sushi);
eat(cordon_bleu, Day::Monday);
eat(steak, Day::Tuesday);
eat(sushi, Day::Wednesday);
}
Voir aussi
16-3. L'enum Result▲
Result est une version plus élaborée de l'enum Option
qui conçoit une potentielle erreur plutôt qu'une potentielle absence.
Autrement dit, Result
<T, E> pourrait adopter l'un de ces deux états :
Ok
<T> : Un élément T a été trouvé ;Err
<E> : Une erreur a été trouvée avec l'élément E.
Par convention, la valeur de retour attendue est Ok
jusqu'à la preuve du contraire (i.e. qu'une erreur (Err
) est survenue).
Tout comme Option
, Result
possède de nombreuses méthodes associées à elle. unwrap(), par exemple, fournit l'élément T sinon déclenche panic. Pour la gestion des cas, Result
possède nombre de combinateurs en commun avec Option
.
En travaillant avec Rust, vous rencontrerez probablement des méthodes renvoyant le type Result
, telles que la méthode parse(). Il n'est pas toujours possible de convertir une chaîne de caractères dans un autre type, donc parse() renvoie une instance de Result
indiquant les potentielles erreurs.
Voyons ce qu'il se passe lorsque nous parvenons à convertir une chaîne de caractères et lorsque ce n'est pas le cas :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
fn
double_number(number_str: &
str) -> i32 {
// Essayons d'utiliser la méthode `unwrap` pour récupérer le nombre.
// Va-t-elle nous mordre ?
2
* number_str.parse::<i32>().unwrap()
}
fn
main() {
let
twenty = double_number("10"
);
println!
("double is
{}
"
, twenty);
let
tt = double_number("t"
);
println!
("double is
{}
"
, tt);
}
Dans le cas où nous ne parvenons pas à la convertir, parse() nous laisse avec l'erreur sur laquelle unwrap a paniqué. Vous noterez également que le message d'erreur affiché par panic est assez désagréable.
Pour améliorer la qualité de notre message d'erreur, nous devrions être plus rigoureux quant à la valeur de retour et envisager de gérer explicitement l'erreur.
16-3-1. La méthode map pour Result▲
Nous avons noté dans l'exemple précédent que le message d'erreur affiché lorsque le programme "panique" ne nous était pas d'une grande aide. Pour éviter cela, nous devons être plus précis concernant le type de renvoi. Ici, l'élément est de type i32. Pour déterminer le type de Err
, jetons un oeil à la documentation de la méthode parse(), qui est implémentée avec le trait FromStr pour le type i32. Dans un résultat, le type de Err
est ParseIntError.
Dans l'exemple ci-dessous, utiliser directement match
mène à coder quelque chose de relativement lourd. Heureusement, la méthode map de Option
est l'un de ces nombreux combinateurs ont également été implémentés pour Result
. enum.Result en tient une liste complète.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
use
std::num::ParseIntError;
// Maintenant que le type de renvoi a été réécrit, nous utilisons le pattern matching
// sans la méthode `unwrap()`.
fn
double_number(number_str: &
str) -> Result
<i32, ParseIntError> {
match
number_str.parse::<i32>() {
Ok
(n) => Ok
(2
* n),
Err
(e) => Err
(e),
}
}
// Tout comme avec `Option`, nous pouvons utiliser les combinateurs tels que `map()`.
// Cette fonction possède le même fonctionnement que celle ci-dessus et se lit comme suit:
// Modifie n si la valeur est valide, sinon renvoie une erreur.
fn
double_number_map(number_str: &
str) -> Result
<i32, ParseIntError> {
number_str.parse::<i32>().map(|n| 2
* n)
}
fn
print(result: Result
<i32, ParseIntError>) {
match
result {
Ok
(n) => println!
("n is
{}
"
, n),
Err
(e) => println!
("Error:
{}
"
, e),
}
}
fn
main() {
// Ceci fournit toujours une réponse valable.
let
twenty = double_number("10"
);
print(twenty);
// Ce qui suit fournit désormais un message d'erreur plus intelligible.
let
tt = double_number_map("t"
);
print(tt);
}
16-3-2. Les alias de Result▲
Quid lorsque nous souhaitons réutiliser un type de Result
bien précis ? Rappelez-vous que Rust nous permet de créer des alias. Nous pouvons alors aisément en définir un pour le type de Result
en question.
À l'échelle d'un module, la création d'alias peut être salvatrice. Les erreurs pouvant être trouvées dans un module pécis ont souvent le même type (wrappé par Err
), donc un seul alias peut définir l'intégralité des Result
s associés. C'est tellement utile que la bibliothèque standard en fournit un: io::Result
!
Voici un petit exemple pour présenter la syntaxe :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
use
std::num::ParseIntError;
// On définit un alias générique pour un type de `Result` avec le type d'erreur
// `ParseIntError`.
type
AliasedResult<T> = Result
<T, ParseIntError>;
// On utilise l'alias ci-dessus pour faire référence à notre
// `Result`.
fn
double_number(number_str: &
str) -> AliasedResult<i32> {
number_str.parse::<i32>().map(|n| 2
* n)
}
// Ici, l'alias nous permet encore d'épargner de l'espace.
fn
print(result: AliasedResult<i32>) {
match
result {
Ok
(n) => println!
("n is
{}
"
, n),
Err
(e) => println!
("Error:
{}
"
, e),
}
}
fn
main() {
print(double_number("10"
));
print(double_number("t"
));
}
Voir aussi
Result et io::Result.
16-4. Multiples types d'erreur▲
Les exemples précédents ont toujours été très basiques; Les (instances de) Result
interagissent avec d'autres (instances de) Result
et les Option
s interagissent avec d'autres Option
s.
Parfois une instance d'Option
a besoin d'interagir avec un Result
ou encore un Result
<T, Error1> devant interagir avec un Result
<T, Error2>. Dans ces cas, nous souhaitons gérer nos différents types de manière à pouvoir interagir simplement avec eux.
Dans le code suivant, deux instances de la méthode unwrap() génèrent deux types d'erreur différents. Vec::first renvoie une instance de Option
, alors que parse::<i32> renvoie une instance de Result
<i32, ParseIntError> :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
fn
double_first(vec: Vec<&
str>) -> i32 {
let
first = vec.first().unwrap(); // Génère la première erreur.
2
* first.parse::<i32>().unwrap() // Génère la seconde erreur.
}
fn
main() {
let
empty = vec!
[];
let
strings = vec!
["tofu"
, "93"
, "18"
];
println!
("The first doubled is
{}
"
, double_first(empty));
// Erreur 1: Le vecteur passé en entrée est vide.
println!
("The first doubled is
{}
"
, double_first(strings));
// Erreur 2: L'élément ne peut pas être converti en nombre.
}
En utilisant notre connaissance des combinateurs, nous pouvons réécrire ce qu'il y a au-dessus pour gérer explicitement les erreurs. Puisque deux types différents peuvent être rencontrés, nous nous devons de les convertir en un type commun tel que String.
Pour ce faire, nous convertissons les instances d'Option
et de Result
en Result
s puis nous convertissons leurs erreurs respectives sous le même type :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
// Nous utiliserons `String` en tant que type d'erreur.
type
Result
<T> = std::result::Result
<T, String>;
fn
double_first(vec: Vec<&
str>) -> Result
<i32> {
vec.first()
// On convertit l'`Option` en un `Result` s'il y a une valeur.
// Autrement, on fournit une instance `Err` contenant la `String`.
.ok_or("Please use a vector with at least one element."
.to_owned())
.and_then(|s| s.parse::<i32>()
// On convertit n'importe quelle erreur, générée par `parse`,
// en `String`.
.map_err(|e| e.to_string())
// `Result<T, String>` est le nouveau type de valeur,
// et nous pouvons doubler le nombre se trouvant dans le conteneur.
.map(|i| 2
* i))
}
fn
print(result: Result
<i32>) {
match
result {
Ok
(n) => println!
("The first doubled is
{}
"
, n),
Err
(e) => println!
("Error:
{}
"
, e),
}
}
fn
main() {
let
empty = vec!
[];
let
strings = vec!
["tofu"
, "93"
, "18"
];
print(double_first(empty));
print(double_first(strings));
}
Dans la prochaine section, nous allons voir une méthode pour la gestion explicite de ces erreurs.
Voir aussi
Option::ok_or, Result::map_err.
16-4-1. Retour prématuré▲
Dans l'exemple précédent, nous gérions explicitement les erreurs en utilisant les combinateurs. Une autre manière de répondre à cette analyse est d'utiliser une série de match
et des retours prématurés.
Autrement dit, nous pouvons simplement mettre fin à l'exécution de la fonction et renvoyer l'erreur, s'il y en a une. Pour certains, cette manière de faire est plus simple à lire et écrire. Voici une nouvelle version de l'exemple précédent, réécrit en utilisant les retours prématurés :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
// On utilise `String` comme type d'erreur.
type
Result
<T> = std::result::Result
<T, String>;
fn
double_first(vec: Vec<&
str>) -> Result
<i32> {
// On convertit l'`Option` en un `Result` s'il y a une valeur.
// Autrement, on fournit une instance `Err` contenant la `String`.
let
first = match
vec.first() {
Some
(first) => first,
None
=> return
Err
("Please use a vector with at least one element."
.to_owned()),
};
// On double le nombre dans le conteneur si `parse` fonctionne
// correctement.
// Sinon, on convertit n'importe quelle erreur, générée par `parse`,
// en `String`.
match
first.parse::<i32>() {
Ok
(i) => Ok
(2
* i),
Err
(e) => Err
(e.to_string()),
}
}
fn
print(result: Result
<i32>) {
match
result {
Ok
(n) => println!
("The first doubled is
{}
"
, n),
Err
(e) => println!
("Error:
{}
"
, e),
}
}
fn
main() {
let
empty = vec!
[];
let
strings = vec!
["tofu"
, "93"
, "18"
];
print(double_first(empty));
print(double_first(strings));
}
À ce niveau, nous avons appris à gérer explicitement les erreurs en utilisant les combinateurs et les retours prématurés. Bien que, généralement, nous souhaitons éviter un plantage, la gestion explicite de toutes nos erreurs peut s'avérer relativement lourde.
Dans la section suivante, nous introduirons la macro try!
pour couvrir les cas où nous souhaitons simplement utiliser unwrap() sans faire planter le programme.
16-4-2. Introduction à try!▲
Parfois nous voulons la simplicité de unwrap() sans la possibilité de faire planter le programme. Jusqu'ici, unwrap() nous a dévié de ce que nous voulions vraiment : récupérer la variable. C'est exactement le but de try!
que de régler ce souci.
Une fois l'instance Err
trouvée, il y a deux actions possibles :
panic!
que nous avons choisi d'éviter, si possible, avectry!
;return
parce qu'uneErr
ne peut être traitée.
La macro try!
est presque(1) équivalent à la méthode unwrap() mais effectue un renvoi prématuré au lieu de planter lorsqu'un conteneur Err
est récupéré. Voyons comment nous pouvons simplifier l'exemple précédent qui utilisait les combinateurs :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
// On utilise une `String` comme type d'erreur.
type
Result
<T> = std::result::Result
<T, String>;
fn
double_first(vec: Vec<&
str>) -> Result
<i32> {
let
first = try!
(vec.first().ok_or(
"Please use a vector with at least one element."
.to_owned(),
));
let
value = try!
(first.parse::<i32>().map_err(|e| e.to_string()));
Ok
(2
* value)
}
fn
print(result: Result
<i32>) {
match
result {
Ok
(n) => println!
("The first doubled is
{}
"
, n),
Err
(e) => println!
("Error:
{}
"
, e),
}
}
fn
main() {
let
empty = vec!
[];
let
strings = vec!
["tofu"
, "93"
, "18"
];
print(double_first(empty));
print(double_first(strings));
}
Notez que, jusqu'ici, nous avons utilisé les Strings pour les erreurs. Cependant, elles sont quelque peu limitées en tant que type d'erreur. Dans la prochaine section, nous apprendrons à créer des erreurs plus structurées, plus riches, en définissant leur propre type.
Voir aussi
Result et io::Result.
16-5. Définition d'un type d'erreur▲
Rust nous permet de définir nos propres types d'erreur. En général, un « bon » type d'erreur :
- représente différentes erreurs avec le même type ;
- présente un message d'erreur intelligible pour l'utilisateur ;
-
est facilement comparable aux autres types ;
- Bien :
Err
(EmptyVec), - Pas bien :
Err
("Please use a vector with at least one element"
.to_owned()) ;
- Bien :
-
peut supporter l'ajout d'informations à propos de l'erreur ;
- Bien :
Err
(BadChar(c, position)), - Pas bien :
Err
("+ cannot be used here"
.to_owned()).
- Bien :
Notez qu'une String (que nous utilisions jusqu'ici) remplit les deux premiers critères, mais pas les deux derniers. Cela rend la création d'erreurs, simplement en utilisant String, verbeuse et difficile à maintenir. Il ne devrait pas être nécessaire de polluer la logique du code avec le formattage des chaînes de caractères pour avoir un affichage intelligible.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
use
std::num::ParseIntError;
use
std::fmt;
type
Result
<T> = std::result::Result
<T, DoubleError>;
#[derive(Debug)]
// On définit nos propres types d'erreur. Ces derniers peuvent être personnalisés pour
// notre gestion des cas.
// Maintenant nous serons capables d'écrire nos propres erreurs et nous reporter
// à leur implémentation.
enum
DoubleError {
// Il n'est pas nécessaire de collecter plus d'informations
// pour détailler cette erreur.
EmptyVec,
// Nous allons nous reporter à l'implémentation couvrant l'erreur de conversion pour ce type.
// Soumettre des informations supplémentaires nécéssite l'ajout de données pour le type.
Parse(ParseIntError),
}
// La génération d'une erreur fait abstraction de la manière dont elle est affichée,
// nul besoin de s'occuper de la mécanique sous-jacente.
//
// Notez que nous ne stockons aucune information supplémentaire à propos de l'erreur.
// Cela signifie que nous ne pouvons préciser quelle chaîne de caractères n'a pas pu être
// convertie, sans modifier nos types pour apporter cette information.
impl
fmt::Display for
DoubleError {
fn
fmt(&
self
, f: &
mut
fmt::Formatter) -> fmt::Result
{
match
*self
{
DoubleError::EmptyVec => write!
(f, "please use a vector with at least one element"
),
// Ceci est un wrapper, donc référrez-vous à l'implémentation de `fmt` respective
// à chaque type.
DoubleError::Parse(ref
e) => e.fmt(f),
}
}
}
fn
double_first(vec: Vec<&
str>) -> Result
<i32> {
vec.first()
// On remplace l'erreur par notre nouveau type.
.ok_or(DoubleError::EmptyVec)
.and_then(|s| s.parse::<i32>()
// On met également à jour le nouveau type d'erreur ici.
.map_err(DoubleError::Parse)
.map(|i| 2
* i))
}
fn
print(result: Result
<i32>) {
match
result {
Ok
(n) => println!
("The first doubled is
{}
"
, n),
Err
(e) => println!
("Error:
{}
"
, e),
}
}
fn
main() {
let
numbers = vec!
["93"
, "18"
];
let
empty = vec!
[];
let
strings = vec!
["tofu"
, "93"
, "18"
];
print(double_first(numbers));
print(double_first(empty));
print(double_first(strings));
}
Voir aussi
Result et io::Result.
16-6. D'autres cas d'utilisation de try!▲
Vous avez remarqué, dans l'exemple précédent, que notre réaction immédiate à l'appel de parse « était » de passer l'erreur de la bibliothèque dans notre nouveau type :
.and_then(|s| s.parse::<i32>()
.map_err(DoubleError::Parse)
Puisque c'est une opération plutôt commune, il ne serait pas inutile qu'elle soit élidée. Hélas, and_then n'étant pas suffisamment flexible pour cela, ce n'est pas possible. Nous pouvons, à la place, utiliser try!
.
La macro try!
a été précédemment présentée comme permettrant la récupération de la ressource (unwrap) ou le renvoi prématuré, si une erreur survient (return
Err
(err)). C'est plus ou moins vrai. En réalité, elle utilise soit unwrap soit return
Err
(From::from(err)). Puisque From::from est un utilitaire permettant la conversion entre différents types, cela signifie que si vous utilisez try!
où l'erreur peut être convertie au type de retour, elle le sera automatiquement.
Ici, nous réécrivons l'exemple précédent en utilisant try!
. Résultat, la méthode map_err disparaîtra lorsque From::from sera implémenté pour notre type d'erreur :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
use
std::num::ParseIntError;
use
std::fmt;
type
Result
<T> = std::result::Result
<T, DoubleError>;
#[derive(Debug)]
enum
DoubleError {
EmptyVec,
Parse(ParseIntError),
}
// On implémente la conversion du type `ParseIntError` au type `DoubleError`.
// La conversion sera automatiquement appelée par `try!` si une instance `ParseIntError`
// doit être convertie en `DoubleError`.
impl
From<ParseIntError> for
DoubleError {
fn
from(err: ParseIntError) -> DoubleError {
DoubleError::Parse(err)
}
}
impl
fmt::Display for
DoubleError {
fn
fmt(&
self
, f: &
mut
fmt::Formatter) -> fmt::Result
{
match
*self
{
DoubleError::EmptyVec =>
write!
(f, "please use a vector with at least one element"
),
DoubleError::Parse(ref
e) => e.fmt(f),
}
}
}
// La même structure qu'avant mais, plutôt que de chaîner les instances `Result`
// et `Option` tout du long, nous utilisons `try!` pour récupérer immédiatement
// la valeur contenue.
fn
double_first(vec: Vec<&
str>) -> Result
<i32> {
// Convertit toujours en `Result` tout en renseignant comment convertir
// un `None`.
let
first = try!
(vec.first().ok_or(DoubleError::EmptyVec));
let
parsed = try!
(first.parse::<i32>());
Ok
(2
* parsed)
}
fn
print(result: Result
<i32>) {
match
result {
Ok
(n) => println!
("The first doubled is
{}
"
, n),
Err
(e) => println!
("Error:
{}
"
, e),
}
}
fn
main() {
let
numbers = vec!
["93"
, "18"
];
let
empty = vec!
[];
let
strings = vec!
["tofu"
, "93"
, "18"
];
print(double_first(numbers));
print(double_first(empty));
print(double_first(strings));
}
C'est effectivement plus acceptable. Comparé à l'original panic, en remplaçant les appels de unwrap par try!
nous conservons une utilisation assez familière, à l'exception que try!
renvoie les types dans un conteneur Result
, rendant leur déstructuration plus abstraite.
Notez toutefois que vous ne devriez pas systématiquement gérer les erreurs de cette manière pour remplacer les appels de unwrap. Cette méthode de gestion d'erreur a triplé le nombre de lignes de code et ne peut pas être considéré comme « simple » (même si la taille du code n'est pas énorme).
En effet, modifier une bibliothèque de 1000 lignes pour remplacer des appels de unwrap et établir une gestion des erreurs plus « propre » pourrait être faisable en une centaine de lignes supplémentaires. En revanche, le recyclage nécessaire en aval ne serait pas évident.
Nombreuses sont les bibliothèques qui pourraient s'en sortir en implémentant seulement Display et ajouter From comme base pour la gestion. Cependant, pour des bibliothèques plus importantes, l'implémentation de la gestion des erreurs peut répondre à des besoins plus spécifiques.
Voir aussi
From::from et try!.
16-7. Boxing des erreurs▲
En implémentant Display et Form pour notre type d'erreur, nous avons usé de presque tous les outils dédiés à la gestion d'erreur de la bibliothèque standard. Nous avons cependant oublié quelque chose : la capacité à simplement Box notre type.
La bibliothèque standard convertit n'importe quel type qui implémente le trait Error et sera pris en charge par le type Box<Error>, via From. Pour l'utilisateur d'une bibliothèque, ceci permet aisément une manoeuvre de ce genre :
fn
foo(...) -> Result
<T, Box<Error>> { ... }
Un utilisateur peut utiliser nombre de bibliothèques externes, chacune fournissant leurs propres types d'erreur. Pour définir un type de Result
<T, E> valide, l'utilisateur a plusieurs options :
- Définir un nouveau wrapper englobant les types d'erreur de la bibliothèque ;
- Convertir les types d'erreur en String ou vers un autre type intermédiaire ;
- Box les types dans Box<Error>.
Le "boxing" du type d'erreur est un choix plutôt habituel. Le problème est que le type de l'erreur sous-jacente est connu à l'exécution et n'est pas déterminé statiquement. Comme mentionné plus haut, tout ce qu'il y a à faire c'est d'implémenter le trait Error :
2.
3.
4.
trait
Error: Debug + Display {
fn
description(&
self
) -> &
str;
fn
cause(&
self
) -> Option
<&
Error>;
}
Avec cette implémentation, jetons un oeil à notre exemple récemment présenté. Notez qu'il est tout aussi fonctionnel avec le type Box<Error> qu'avec DoubleError :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
use
std::error;
use
std::fmt;
use
std::num::ParseIntError;
// On modifie l'alias pour ajouter `Box<error::Error>`.
type
Result
<T> = std::result::Result
<T, Box<error::Error>>;
#[derive(Debug)]
enum
DoubleError {
EmptyVec,
Parse(ParseIntError),
}
impl
From<ParseIntError> for
DoubleError {
fn
from(err: ParseIntError) -> DoubleError {
DoubleError::Parse(err)
}
}
impl
fmt::Display for
DoubleError {
fn
fmt(&
self
, f: &
mut
fmt::Formatter) -> fmt::Result
{
match
*self
{
DoubleError::EmptyVec =>
write!
(f, "please use a vector with at least one element"
),
DoubleError::Parse(ref
e) => e.fmt(f),
}
}
}
impl
error::Error for
DoubleError {
fn
description(&
self
) -> &
str {
match
*self
{
// Courte description de l'erreur.
// Vous n'êtes pas obligé de renseigner la même description que
// pour `Display`.
DoubleError::EmptyVec => "empty vectors not allowed"
,
// Ceci implémente déjà `Error`, on se reporte à sa propre implémentation.
DoubleError::Parse(ref
e) => e.description(),
}
}
fn
cause(&
self
) -> Option
<&
error::Error> {
match
*self
{
// Pas de cause (i.e. pas d'autre erreur)
// sous-jacente au déclenchement de cette erreur, donc on renvoie `None`.
DoubleError::EmptyVec => None
,
// La cause est l'implémentation sous-jacente du type d'erreur.
// Il (le type) est implicitement casté en `&error::Error`.
// Ca fonctionne parce que le type en question a déjà implémenté le trait `Error`.
DoubleError::Parse(ref
e) => Some
(e),
}
}
}
fn
double_first(vec: Vec<&
str>) -> Result
<i32> {
let
first = try!
(vec.first().ok_or(DoubleError::EmptyVec));
let
parsed = try!
(first.parse::<i32>());
Ok
(2
* parsed)
}
fn
print(result: Result
<i32>) {
match
result {
Ok
(n) => println!
("The first doubled is
{}
"
, n),
Err
(e) => println!
("Error:
{}
"
, e),
}
}
fn
main() {
let
numbers = vec!
["93"
, "18"
];
let
empty = vec!
[];
let
strings = vec!
["tofu"
, "93"
, "18"
];
print(double_first(numbers));
print(double_first(empty));
print(double_first(strings));
}
Voir aussi