IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Rust par l'exemple


précédentsommairesuivant

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 :

 
Sélectionnez
1.
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é.

 
Sélectionnez
1.
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 Options. 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.

 
Sélectionnez
1.
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().

 
Sélectionnez
1.
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

Les closures, Option et Option::and_then().

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 :

 
Sélectionnez
1.
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.

 
Sélectionnez
1.
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 Results 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 :

 
Sélectionnez
1.
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 Options interagissent avec d'autres Options.

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

 
Sélectionnez
1.
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 Results puis nous convertissons leurs erreurs respectives sous le même type :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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 :

  1. panic! que nous avons choisi d'éviter, si possible, avec try! ;
  2. return parce qu'une Err 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 :

 
Sélectionnez
1.
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()) ;
  • peut supporter l'ajout d'informations à propos de l'erreur ;

    • Bien : Err(BadChar(c, position)),
    • Pas bien : Err("+ cannot be used here".to_owned()).

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.

 
Sélectionnez
1.
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 :

 
Sélectionnez
.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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
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 :

 
Sélectionnez
1.
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 :

 
Sélectionnez
1.
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

Distribution dynamique et le trait Error.


précédentsommairesuivant
Consultez cette section pour plus de détails.

Licence Creative Commons
Le contenu de cet article est rédigé par Rust Core Team et est mis à disposition selon les termes de la Licence Creative Commons Attribution 3.0 non transposé.
Les logos Developpez.com, en-tête, pied de page, css, et look & feel de l'article sont Copyright © 2013 Developpez.com.