web-dev-qa-db-fra.com

Comment définissez-vous les types `Error` personnalisés dans Rust?

J'écris une fonction qui pourrait renvoyer plusieurs erreurs différentes.

fn foo(...) -> Result<..., MyError> {}

J'aurai probablement besoin de définir mon propre type d'erreur pour représenter ces erreurs. Je suppose que ce serait une enum d'erreurs possibles, certaines variantes de enum étant associées à des données de diagnostic:

enum MyError {
    GizmoError,
    WidgetNotFoundError(widget_name: String)
}

Est-ce la façon la plus idiomatique de s'y prendre? Et comment puis-je implémenter le trait Error?

22
Jo Liss

Vous implémentez Error exactement comme vous le feriez tout autre trait ; il n'y a rien de très spécial à ce sujet:

pub trait Error: Debug + Display {
    fn description(&self) -> &str { /* ... */ }
    fn cause(&self) -> Option<&Error> { /* ... */ }
    fn source(&self) -> Option<&(Error + 'static)> { /* ... */ }
}

description, cause et source ont tous des implémentations par défaut1et votre type doit également implémenter Debug et Display, car il s’agit de supertraits.

use std::{error::Error, fmt};

#[derive(Debug)]
struct Thing;

impl Error for Thing {}

impl fmt::Display for Thing {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Oh no, something bad went down")
    }
}

Bien sûr, ce que contient Thing, et donc l’implémentation des méthodes, dépend fortement du type d’erreur que vous souhaitez commettre. Peut-être que vous souhaitez inclure un nom de fichier, ou peut-être un entier. Peut-être voudriez-vous avoir une enum au lieu de struct pour représenter plusieurs types d’erreurs.

Si vous finissez par envelopper les erreurs existantes, nous vous recommandons d'implémenter From pour convertir ces erreurs en votre erreur. Cela vous permet d’utiliser try! et ? et d’avoir une jolie solution ergonomique.

Est-ce la façon la plus idiomatique de s'y prendre?

Idéalement, je dirais qu'une bibliothèque aura un petit nombre (peut-être 1 à 3) de types d'erreur principaux exposés. Celles-ci sont probablement des énumérations d'autres types d'erreur. Cela permet aux consommateurs de votre caisse de ne pas faire face à une explosion de types. Bien sûr, cela dépend de votre API et de la pertinence de regrouper certaines erreurs ou non.

Une autre chose à noter est que, lorsque vous choisissez d'incorporer des données dans l'erreur, cela peut avoir de lourdes conséquences. Par exemple, la bibliothèque standard n'inclut pas de nom de fichier dans les erreurs liées au fichier. Cela ajouterait une surcharge à chaque erreur de fichier. L'appelant de la méthode a généralement le contexte approprié et peut décider si ce contexte doit être ajouté à l'erreur ou non.


Je recommanderais de le faire à la main plusieurs fois pour voir comment toutes les pièces vont ensemble. Une fois que vous avez cela, vous en aurez assez de le faire manuellement. Ensuite, vous pouvez consulter des caisses telles que quick-error , error-chain ou failure , qui fournissent des macros permettant de réduire le passe-partout.

Ma bibliothèque préférée est quick-error, alors voici un exemple d'utilisation avec le type d'erreur d'origine:

#[macro_use]
extern crate quick_error;

quick_error! {
    #[derive(Debug)]
    enum MyError {
        Gizmo {
            description("Refrob the Gizmo")
        }
        WidgetNotFound(widget_name: String) {
            description("The widget could not be found")
            display(r#"The widget "{}" could not be found"#, widget_name)
        }
    }
}

fn foo() -> Result<(), MyError> {
    Err(MyError::WidgetNotFound("Quux".to_string()))
}

fn main() {
    println!("{:?}", foo());
}

Remarque J'ai supprimé le suffixe redondant Error sur chaque valeur enum. Il est également courant d'appeler simplement le type Error et de permettre au consommateur de préfixer le type (mycrate::Error) ou de le renommer à l'importation (use mycrate::Error as FooError).


1 Avant que RFC 2504 soit implémenté, description était une méthode obligatoire.

29
Shepmaster

Est-ce la façon la plus idiomatique de s'y prendre? Et comment puis-je implémenter le trait Erreur?

C'est un moyen commun, oui. "idiomatique" dépend de la force avec laquelle vous voulez que vos erreurs soient, et de la façon dont vous voulez que cela interagisse avec d'autres choses.

Et comment puis-je implémenter le trait Erreur?

Strictement parlant, vous n’avez pas besoin d’être ici. Vous pouvez éventuellement assurer l’interopérabilité avec d’autres éléments nécessitant Error, mais puisque vous avez défini votre type de retour directement sous cette énumération, votre code doit fonctionner sans lui.

1
Steve Klabnik

La caisse custom_error permet la définition de types d'erreur personnalisés avec moins de passe-passe que ce qui était proposé ci-dessus:

custom_error!{MyError
     Io{source: io::Error}             = "input/output error",
     WidgetNotFoundError{name: String} = "could not find widget '{name}'",
     GizmoError                        = "A gizmo error occurred!"
}

Disclaimer: Je suis l'auteur de cette caisse.

1
lovasoa