Existe-t-il des raisons légitimes de retourner des objets d'exception au lieu de les lancer?

45

Cette question est destinée à s'appliquer à tout langage de programmation orienté objet prenant en charge la gestion des exceptions; J'utilise C # à des fins d'illustration uniquement.

Les exceptions sont généralement destinées à être déclenchées lorsqu'un problème que le code ne peut pas résoudre immédiatement, puis à être interceptées dans une catchclause située à un emplacement différent (généralement un cadre de pile externe).

Q: Existe-t-il des situations légitimes dans lesquelles des exceptions ne sont pas levées ni interceptées, mais simplement renvoyées par une méthode, puis transmises en tant qu'objets d'erreur?

Cette question m’a été posée car la System.IObserver<T>.OnErrorméthode de .NET 4 le suggère: les exceptions sont transmises comme des objets d’erreur.

Regardons un autre scénario, la validation. Supposons que je suis la sagesse conventionnelle et que je distingue donc un type d'objet d'erreur IValidationErroret un type d'exception distinct ValidationExceptionutilisé pour signaler des erreurs inattendues:

partial interface IValidationError { }

abstract partial class ValidationException : System.Exception
{
    public abstract IValidationError[] ValidationErrors { get; }
}

(L' System.Component.DataAnnotationsespace de nom fait quelque chose d'assez similaire.)

Ces types pourraient être utilisés comme suit:

partial interface IFoo { }  // an immutable type

partial interface IFooBuilder  // mutable counterpart to prepare instances of above type
{
    bool IsValid(out IValidationError[] validationErrors);  // true if no validation error occurs
    IFoo Build();  // throws ValidationException if !IsValid(…)
}

Maintenant, je me demande si je ne peux pas simplifier ce qui précède à ceci:

partial class ValidationError : System.Exception { }  // = IValidationError + ValidationException

partial interface IFoo { }  // (unchanged)

partial interface IFooBuilder
{
    bool IsValid(out ValidationError[] validationErrors);
    IFoo Build();  // may throw ValidationError or sth. like AggregateException<ValidationError>
}

Q: Quels sont les avantages et les inconvénients de ces deux approches différentes?

Stakx
la source
Si vous cherchez deux réponses distinctes, vous devriez poser deux questions distinctes. En les combinant, il est encore plus difficile de répondre.
Bobson
2
@Bobson: Je considère ces deux questions comme complémentaires: la première question est censée définir le contexte général de la question en termes généraux, tandis que la seconde demande des informations spécifiques qui devraient permettre aux lecteurs de tirer leurs propres conclusions. Une réponse n'a pas à donner de réponses distinctes et explicites aux deux questions; les réponses "entre-deux" sont également les bienvenues.
Stakx
5
Je ne suis pas clair sur un point: quelle est la raison de dériver ValidationError d’Exception, si vous n’allez pas le lancer?
pdr
@pdr: Voulez-vous dire dans le cas de lancer à la AggregateExceptionplace? Bon point.
Stakx
3
Pas vraiment. Je veux dire que si vous voulez le lancer, utilisez une exception. Si vous voulez le retourner, utilisez un objet d'erreur. Si vous attendez des exceptions qui sont réellement des erreurs par nature, récupérez-les et mettez-les dans votre objet d'erreur. Que l'une ou l'autre autorise ou non plusieurs erreurs n'a aucune pertinence.
pdr

Réponses:

31

Existe-t-il des situations légitimes dans lesquelles des exceptions ne sont pas levées et capturées, mais simplement renvoyées par une méthode et ensuite transmises en tant qu'objets d'erreur?

S'il n'est jamais lancé, il ne s'agit pas d'une exception. C'est un objet derivedd'une classe Exception, bien qu'il ne suive pas le comportement. L'appeler une exception est purement sémantique à ce stade, mais je ne vois rien de mal à ne pas la lancer. De mon point de vue, ne pas lancer d'exception est exactement la même chose qu'une fonction interne qui l'attrape et l'empêche de se propager.

Est-ce valide pour une fonction de retourner des objets d'exception?

Absolument. Voici une courte liste d'exemples où cela peut être approprié:

  • Une usine d'exception.
  • Un objet de contexte qui signale s'il y avait une erreur précédente en tant qu'exception prête à l'emploi.
  • Une fonction qui garde une exception précédemment capturée.
  • API tierce qui crée une exception d'un type interne.

Est-ce que le jeter n'est pas mauvais?

Lancer une exception, c'est un peu comme: "Va en prison. Ne passe pas Go!" dans le jeu de société Monopoly. Il demande à un compilateur de passer par-dessus tout le code source sans avoir à exécuter ce code source. Cela n'a rien à voir avec des erreurs, rapporter ou arrêter des bogues. J'aime penser à lancer une exception en tant qu'instruction "super return" pour une fonction. Il renvoie l'exécution du code à un niveau beaucoup plus élevé que l'appelant d'origine.

L'important ici est de comprendre que la vraie valeur des exceptions réside dans le try/catchmodèle, et non dans l'instanciation de l'objet exception. L'objet exception est juste un message d'état.

Dans votre question, vous semblez confondre l'utilisation de ces deux choses: un saut vers un gestionnaire de l'exception et l'état d'erreur représenté par l'exception. Le fait que vous ayez pris un état d'erreur et que vous l'ayez enveloppé dans une exception ne signifie pas que vous suivez le try/catchmodèle ou ses avantages.

Réactionnel
la source
4
"S'il n'est jamais lancé, il ne s'agit pas d'une exception. C'est un objet dérivé d'une Exceptionclasse mais qui ne suit pas le comportement." - Et c'est exactement le problème: est-il légitime d'utiliser un Exceptionobjet dérivé dans une situation où il ne sera pas projeté du tout, ni par le producteur de l'objet, ni par aucune méthode d'appel; où il sert uniquement de "message d'état", sans le flux de contrôle "super-retour"? Ou suis-je en train de violer un contrat try/ catch(Exception)contrat tacite au point que je ne devrais jamais le faire, mais plutôt utiliser un type différent, non Exceptiontypé?
Stakx
10
Les programmeurs de @stakx ont et continueront de faire des choses qui déroutent les autres, mais qui ne sont pas techniquement incorrectes. C'est pourquoi j'ai dit que c'était de la sémantique. La réponse légale est que Novous ne rompez aucun contrat. Le popular opinionserait que vous ne devriez pas faire ça. La programmation est pleine d'exemples où votre code source ne fait rien de mal, mais tout le monde l'aime pas. Le code source doit toujours être écrit de manière à préciser l’intention. Aidez-vous vraiment un autre programmeur en utilisant une exception de cette façon? Si oui, alors faites-le. Sinon, ne soyez pas surpris si c'est détesté.
Reactgular
@cgTag L'utilisation de blocs de code en ligne pour l'italique me fait mal au cerveau.
Frayt
51

Renvoyer des exceptions au lieu de les lancer peut avoir un sens sémantique lorsque vous disposez d'une méthode d'assistance pour analyser la situation et renvoyer une exception appropriée qui est ensuite levée par l'appelant (vous pouvez appeler cela une "fabrique d'exceptions"). Lancer une exception dans cette fonction d'analyse d'erreur signifierait que quelque chose n'allait pas au cours de l'analyse, alors que renvoyer une exception signifiait que le type d'erreur avait été analysé avec succès.

Un cas d'utilisation possible pourrait être une fonction qui convertit les codes de réponse HTTP en exceptions:

Exception analyzeHttpError(int errorCode) {
    if (errorCode < 400) {
         throw new NotAnErrorException();
    }
    switch (errorCode) {
        case 403:
             return new ForbiddenException();
        case 404:
             return new NotFoundException();
        case 500:
             return new InternalServerErrorException();
        …
        default:
             throw new UnknownHttpErrorCodeException(errorCode);
     }
}

Notez que le lancement d' une exception signifie que la méthode a été mal utilisée ou qu'il y a eu une erreur interne. Le renvoi d' une exception signifie que le code d'erreur a été identifié avec succès.

Philipp
la source
Cela aide également le compilateur à comprendre le flux de code: si une méthode ne retourne jamais correctement (car elle lève l'exception "désirée") et si le compilateur ne le sait pas, vous aurez parfois besoin d' returninstructions superflues pour que le compilateur arrête de se plaindre.
Joachim Sauer
@ JoachimSauer Ouais. Plus d’une fois, j’aimerais souhaiter qu’ils ajoutent un type de retour "jamais" à C # - cela indiquerait que la fonction doit quitter en lançant, y mettre un retour est une erreur. Parfois, vous avez un code dont le seul travail est de créer une exception.
Loren Pechtel
2
@ LorenPechtel Une fonction qui ne retourne jamais n'est pas une fonction. C'est un terminateur. Appeler une fonction comme ça efface la pile d'appels. C'est comme appeler System.Exit(). Le code après l'appel de la fonction n'est pas exécuté. Cela ne semble pas être un bon modèle pour une fonction.
Reactgular
5
@MathewFoscarini Considérez une classe comportant plusieurs méthodes pouvant générer des erreurs de la même manière. Vous souhaitez vider certaines informations d'état dans le cadre de l'exception pour indiquer ce qui se passait quand il a explosé. Vous voulez une copie du code de préparation en raison de DRY. Cela laisse soit le fait d'appeler une fonction qui s'occupe de la mise en page et d'utiliser le résultat dans votre projection, soit une fonction qui construit le texte préparé puis le lance. Je trouve généralement ce dernier supérieur.
Loren Pechtel
1
@ LorenPechtel ah, oui je l'ai fait aussi. Ok je comprends maintenant.
Reactgular
5

Conceptuellement, si l'objet exception est le résultat attendu de l'opération, alors oui. Les cas auxquels je peux penser, cependant, impliquent toujours une attrape-lancer à un moment donné:

  • Variations sur le modèle "Try" (où une méthode encapsule une seconde méthode de levée d'exception, mais capture l'exception et renvoie à la place un booléen indiquant la réussite). Au lieu de renvoyer un booléen, vous pouvez renvoyer n'importe quelle exception levée (null si elle réussit), ce qui permet à l'utilisateur final de disposer de plus d'informations tout en conservant l'indication de réussite ou d'échec sans accroc.

  • Traitement des erreurs de workflow. Semblable dans la pratique à l’encapsulation de la méthode "essayer un modèle", une étape de flux de travail peut être abstraite dans un modèle de chaîne de commande. Si un lien dans la chaîne lève une exception, il est souvent plus propre d'attraper cette exception dans l'objet abstraite, puis la renvoie à la suite de ladite opération pour que le moteur de flux de travail et le code exécuté en tant qu'étape de flux de travail ne nécessitent pas de nombreuses tentatives. -catch logique de leur propre.

KeithS
la source
Je ne pense pas que des personnes notables aient préconisé une telle variation du modèle "Try", mais l'approche "recommandée" consistant à renvoyer un booléen semble plutôt anémique. Je pense qu'il serait peut-être préférable d'avoir une OperationResultclasse abstraite , avec une boolpropriété "Successful", une GetExceptionIfAnyméthode et une AssertSuccessfulméthode (qui lèverait l'exception de GetExceptionIfAnysi non-null). Le fait d’ GetExceptionIfAnyêtre une méthode plutôt qu’une propriété permettrait l’utilisation d’objets d’erreur statiques immuables dans les cas où il n’y aurait pas grand-chose à dire.
Supercat
5

Disons que vous avez chargé une tâche à exécuter dans un pool de threads. Si cette tâche lève une exception, il s'agit d'un thread différent pour ne pas l'attraper. Fil où il a été exécuté vient de mourir.

Considérons maintenant que quelque chose (que ce soit le code de cette tâche ou l'implémentation du pool de threads) intercepte cette exception, le stocke avec la tâche et considère la tâche comme terminée (sans succès). Vous pouvez maintenant demander si la tâche est terminée (et demander si elle a été lancée ou si elle peut être renvoyée dans votre fil de discussion (ou, mieux, une nouvelle exception avec la cause originale comme cause)).

Si vous le faites manuellement, vous remarquerez que vous créez une nouvelle exception, la lançant et la capturant, puis la stockant, dans un fil différent, récupérant le lancer, le rattrapant et le faisant réagir. Il peut donc être judicieux d’éviter de lancer et d’attraper, de simplement l’enregistrer et d’achever la tâche, puis de demander s’il existe une exception et d’y réagir. Mais cela peut conduire à un code plus compliqué si, au même endroit, il peut y avoir de véritables exceptions.

PS: ceci est écrit avec une expérience de Java, où les informations de trace de pile sont créées lors de la création d’une exception (contrairement à C # où elles sont créées lors d’une projection). Ainsi, l'approche d'exception non levée en Java sera plus lente qu'en C # (sauf si elle est pré-créée et réutilisée), mais des informations de trace de pile sont disponibles.

En général, je m'abstiendrais de créer une exception et de ne jamais la lancer (à moins que l'optimisation des performances après le profilage ne l'indique comme un goulot d'étranglement). Au moins en Java, où la création d'exceptions est coûteuse (trace de pile). En C #, c'est une possibilité, mais IMO est surprenant et doit donc être évité.

utilisateur470365
la source
Cela se produit souvent aussi avec les objets écouteurs d'erreurs. Une file d'attente exécute la tâche, intercepte toute exception, puis transmet cette exception au programme d'écoute d'erreur responsable. En général, il n’est pas logique de supprimer le fil de tâche qui ne connaît pas grand chose au travail dans ce cas. Ce type d'idée est également intégré aux API Java où un thread peut obtenir des exceptions transmises par un autre: docs.oracle.com/javase/1.5.0/docs/api/java/lang/…
Lance Nanek
1

Cela dépend de votre conception. En règle générale, je ne renverrais pas les exceptions à un appelant, je les jetterais et les rattraperais, sans en rester là. Généralement, le code est écrit pour échouer rapidement et rapidement. Par exemple, considérons le cas de l'ouverture d'un fichier et de son traitement (C # PsuedoCode):

        private static void ProcessFileFailFast()
        {
            try
            {
                using (var file = new System.IO.StreamReader("c:\\test.txt"))
                {
                    string line;
                    while ((line = file.ReadLine()) != null)
                    {
                        ProcessLine(line);
                    }
                }
            }
            catch (Exception ex) 
            {
                LogException(ex);
            }
        }

        private static void ProcessLine(string line)
        {
            //TODO:  Process Line
        }

        private static void LogException(Exception ex)
        {
            //TODO:  Log Exception
        }

Dans ce cas, nous éviterions les erreurs et arrêterions le traitement du fichier dès qu’il rencontrait un mauvais enregistrement.

Mais disons que l’exigence est que nous souhaitons continuer à traiter le fichier même si une ou plusieurs lignes comportent une erreur. Le code peut commencer à ressembler à ceci:

    private static void ProcessFileFailAndContinue()
    {
        try
        {
            using (var file = new System.IO.StreamReader("c:\\test.txt"))
            {
                string line;
                while ((line = file.ReadLine()) != null)
                {
                    Exception ex = ProcessLineReturnException(line);
                    if (ex != null)
                    {
                        _Errors.Add(ex);
                    }
                }
            }

            //Do something with _Errors Here
        }
        //Catch errors specifically around opening the file
        catch (System.IO.FileNotFoundException fnfe) 
        { 
            LogException(fnfe);
        }    
    }

    private static Exception ProcessLineReturnException(string line)
    {
        try
        {
            //TODO:  Process Line
        }
        catch (Exception ex)
        {
            LogException(ex);
            return ex;
        }

        return null;
    }

Donc, dans ce cas, nous renvoyons l'exception à l'appelant, bien que je ne retourne probablement pas d'exception, mais plutôt une sorte d'objet d'erreur car l'exception a été interceptée et déjà traitée. Cela ne fait pas de mal de renvoyer une exception, mais les autres appelants peuvent renvoyer l'exception qui peut ne pas être souhaitable, car l'objet exception a ce comportement. Si vous souhaitez que les appelants aient la possibilité de lancer à nouveau puis de renvoyer une exception, sinon, retirez les informations de l'objet exception et construisez un objet plus léger et plus léger, puis retournez-le. Le rapide échec est généralement un code plus propre.

Pour votre exemple de validation, je n’aurais probablement pas hérité d’une classe d’exception ni d’exceptions car les erreurs de validation pourraient être assez courantes. Il serait moins fastidieux de retourner un objet plutôt que de lancer une exception si 50% de mes utilisateurs ne remplissent pas correctement le formulaire à la première tentative.

Jon Raynor
la source
1

Q: Existe-t-il des situations légitimes dans lesquelles des exceptions ne sont pas levées ni interceptées, mais simplement renvoyées par une méthode, puis transmises en tant qu'objets d'erreur?

Oui. Par exemple, j'ai récemment rencontré une situation dans laquelle j'ai constaté que les exceptions levées dans un service Web ASMX n'incluaient pas d'élément dans le message SOAP résultant. Je devais donc le générer.

Code illustratif:

Public Sub SomeWebMethod()
    Try
        ...
    Catch ex As Exception
        Dim soapex As SoapException = Me.GenerateSoapFault(ex)
        Throw soapex
    End Try
End Sub

Private Function GenerateSoapFault(ex As Exception) As SoapException
    Dim document As XmlDocument = New XmlDocument()
    Dim faultDetails As XmlNode = document.CreateNode(XmlNodeType.Element, SoapException.DetailElementName.Name, SoapException.DetailElementName.Namespace)
    faultDetails.InnerText = ex.Message
    Dim exceptionType As String = ex.GetType().ToString()
    Dim soapex As SoapException = New SoapException("SoapException", SoapException.ClientFaultCode, Context.Request.Url.ToString, faultDetails)
    Return soapex
End Function
Tom Tom
la source
1

Dans la plupart des langues (autant que je sache), des exceptions viennent avec quelques goodies ajoutés. Généralement, un instantané de la trace de pile actuelle est stocké dans l'objet. C'est bien pour le débogage, mais cela peut aussi avoir des implications sur la mémoire.

Comme @ThinkingMedia l'a déjà dit, vous utilisez réellement l'exception comme objet d'erreur.

Dans votre exemple de code, il semble que vous le fassiez principalement pour la réutilisation du code et pour éviter la composition. Personnellement, je ne pense pas que ce soit une bonne raison de faire cela.

Une autre raison possible serait de tromper le langage pour vous donner un objet d'erreur avec une trace de pile. Cela donne des informations contextuelles supplémentaires à votre code de traitement des erreurs.

D'autre part, nous pouvons supposer que garder la trace de la pile a un coût en mémoire. Par exemple, si vous commencez à collecter ces objets quelque part, vous risquez de subir un impact désagréable sur la mémoire. Bien entendu, cela dépend en grande partie de la manière dont les traces de pile d'exceptions sont implémentées dans le langage / moteur correspondant.

Alors, est-ce une bonne idée?

Jusqu'à présent, je ne l'ai pas vu utilisé ou recommandé. Mais cela ne veut pas dire grand chose.

La question est: la trace de pile est-elle vraiment utile pour la gestion de vos erreurs? Ou plus généralement, quelles informations souhaitez-vous fournir au développeur ou à l'administrateur?

Le motif typique lancer / essayer / attraper rend nécessaire d'avoir une trace de pile, car sinon, vous n'avez aucune idée de sa provenance. Pour un objet retourné, il provient toujours de la fonction appelée. La trace de la pile peut contenir toutes les informations dont le développeur a besoin, mais une solution moins lourde et plus spécifique suffirait peut-être.

don Quichotte
la source
-4

La réponse est oui. Voir http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Exceptions et ouvrez la flèche du guide de style C ++ de Google sur les exceptions. Vous pouvez voir ici les arguments contre lesquels ils avaient l'habitude de se prononcer, mais dites que s'ils devaient le refaire, ils auraient probablement décidé de le faire.

Toutefois, il convient de noter que le langage Go n'utilise pas non plus idiomatiquement les exceptions, pour des raisons similaires.

billy
la source
1
Désolé, mais je ne comprends pas en quoi ces instructions aident à répondre à la question: elles recommandent simplement que la gestion des exceptions soit complètement évitée. Ce n'est pas ce que je demande. Ma question est de savoir s'il est légitime d'utiliser un type d'exception pour décrire une condition d'erreur dans une situation où l'objet d'exception résultant ne sera jamais levé. Il semble n'y avoir rien à ce sujet dans ces directives.
Stakx