Est-il acceptable d'utiliser les exceptions comme outils pour «détecter» les erreurs tôt?

29

J'utilise des exceptions pour détecter les problèmes tôt. Par exemple:

public int getAverageAge(Person p1, Person p2){
    if(p1 == null || p2 == null)
        throw new IllegalArgumentException("One or more of input persons is null").
    return (p1.getAge() + p2.getAge()) / 2;
}

Mon programme ne doit jamais passer nulldans cette fonction. Je n'en ai jamais l'intention. Cependant, comme nous le savons tous, des choses imprévues se produisent dans la programmation.

Lancer une exception si ce problème se produit, me permet de le repérer et de le corriger, avant qu'il ne cause plus de problèmes à d'autres endroits du programme. L'exception arrête le programme et me dit "de mauvaises choses sont arrivées ici, corrigez-le". Au lieu de cela, le nulldéplacement du programme provoque des problèmes ailleurs.

Maintenant, vous avez raison, dans ce cas, nullcela provoquerait tout de suite un problème NullPointerException, donc ce n'est peut-être pas le meilleur exemple.

Mais considérons une méthode comme celle-ci par exemple:

public void registerPerson(Person person){
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

Dans ce cas, un nullparamètre as serait transmis au programme et pourrait provoquer des erreurs beaucoup plus tard, qui seront difficiles à retracer jusqu'à leur origine.

Changer la fonction comme ceci:

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

Me permet de repérer le problème bien avant qu'il ne provoque des erreurs étranges dans d'autres endroits.

De plus, une nullréférence en tant que paramètre n'est qu'un exemple. Il peut y avoir plusieurs types de problèmes, des arguments invalides à autre chose. Il vaut toujours mieux les repérer tôt.


Ma question est donc simplement: est-ce une bonne pratique? Mon utilisation des exceptions comme outils de prévention des problèmes est-elle bonne? Est-ce une application légitime des exceptions ou est-ce problématique?

Aviv Cohn
la source
2
Le downvoter peut-il expliquer ce qui ne va pas avec cette question?
Giorgio
2
"crash tôt, crash souvent"
Bryan Chen
16
C'est littéralement pour cela qu'il y a des exceptions.
raptortech97
Notez que les contrats de codage vous permettent de détecter de nombreuses erreurs de ce type de manière statique. Ceci est généralement supérieur au lancement d'une exception lors de l'exécution. La prise en charge et l'efficacité des contrats de codage varient selon la langue.
Brian
3
Étant donné que vous lancez IllegalArgumentException, il est redondant de dire « Input person».
Kevin Cline du

Réponses:

29

Oui, "échouer tôt" est un très bon principe, et c'est simplement une façon possible de le mettre en œuvre. Et dans les méthodes qui doivent renvoyer une valeur spécifique, il n'y a pas grand-chose d'autre pouvez faire échouer délibérément - il est soit lancer des exceptions, ou le déclenchement d' assertions. Les exceptions sont censées signaler des conditions «exceptionnelles», et la détection d'une erreur de programmation est certainement exceptionnelle.

Kilian Foth
la source
Échouer tôt est la voie à suivre s'il n'y a aucun moyen de se remettre d'une erreur ou d'une condition. Par exemple, si vous devez copier un fichier et que le chemin source ou de destination est vide, vous devez immédiatement lancer une exception.
Michael Shopsin du
Il est également connu sous le nom de «échec rapide»
keuleJ
8

Oui, lever des exceptions est une bonne idée. Jetez-les tôt, jetez-les souvent, jetez-les avec impatience.

Je sais qu'il y a un débat "exceptions vs assertions", avec certains types de comportements exceptionnels (en particulier, ceux qui reflètent des erreurs de programmation) gérés par des assertions qui peuvent être "compilées" pour l'exécution plutôt que de déboguer / tester des builds. Mais la quantité de performances consommée dans quelques vérifications de correction supplémentaires est minime sur le matériel moderne, et tout coût supplémentaire est largement compensé par la valeur d'avoir des résultats corrects et non corrompus. Je n'ai jamais rencontré de base de code d'application pour laquelle je souhaiterais que la plupart des vérifications soient supprimées lors de l'exécution.

Je suis tenté de dire que je ne voudrais pas beaucoup de vérifications et de conditions supplémentaires dans les boucles serrées du code numériquement intensif ... mais c'est en fait là que beaucoup d'erreurs numériques sont générées, et si elles ne sont pas détectées, elles se propageront vers l'extérieur effectuer tous les résultats. Même là-bas, les contrôles valent la peine. En fait, certains des meilleurs algorithmes numériques les plus efficaces sont basés sur l'évaluation des erreurs.

Un dernier endroit pour être très conscient du code supplémentaire est le code très sensible à la latence, où des conditions supplémentaires peuvent provoquer des blocages de pipeline. Ainsi, au milieu du système d'exploitation, du SGBD et d'autres noyaux de middleware, et de la communication de bas niveau / gestion des protocoles. Mais là encore, ce sont certains des endroits où les erreurs sont les plus susceptibles d'être observées, et leurs effets (sécurité, exactitude et intégrité des données) sont les plus dommageables.

Une amélioration que j'ai trouvée est de ne pas lever uniquement les exceptions de niveau de base. IllegalArgumentExceptionc'est bien, mais ça peut venir de pratiquement n'importe où. Il ne faut pas grand-chose dans la plupart des langues pour ajouter des exceptions personnalisées. Pour votre module de gestion des personnes, dites:

public class PersonArgumentException extends IllegalArgumentException {
    public MyException(String message) {
        super(message);
    }
}

Ensuite, quand quelqu'un voit un PersonArgumentException, c'est clair d'où ça vient. Il y a un équilibre entre le nombre d'exceptions personnalisées que vous souhaitez ajouter, car vous ne voulez pas multiplier les entités inutilement (Rasoir d'Occam). Souvent, quelques exceptions personnalisées suffisent pour signaler "ce module n'obtient pas les bonnes données!" ou "ce module ne peut pas faire ce qu'il est censé faire!" d'une manière qui est spécifique et adaptée, mais pas trop précise que vous devez réimplémenter la hiérarchie d'exceptions entière. J'arrive souvent à ce petit ensemble d'exceptions personnalisées en commençant par les exceptions de stock, puis en analysant le code et en réalisant que "ces N endroits soulèvent des exceptions de stock, mais ils se résument à l'idée de niveau supérieur qu'ils n'obtiennent pas les données ils ont besoin; laissez '

Jonathan Eunice
la source
I've never actually met an application codebase for which I'd want (most) checks removed at runtime. Ensuite, vous n'avez pas fait de code critique pour les performances. Je travaille actuellement sur quelque chose qui fait 37 millions d'opérations par seconde avec des assertions compilées sur et 42 millions sans elles. Les assertions ne valident pas l'entrée extérieure, elles sont là pour s'assurer que le code est correct. Mes clients sont plus qu'heureux de profiter de l'augmentation de 13% une fois que je suis convaincu que mes affaires ne sont pas cassées.
Blrfl
Il faut éviter d'apparier une base de code avec des exceptions personnalisées, car même si cela peut vous aider, cela n'aide pas les autres qui doivent en prendre connaissance. Habituellement, si vous vous trouvez à étendre une exception commune et à ne pas ajouter de membres ou de fonctionnalités, vous n'en avez pas réellement besoin. Même dans votre exemple, ce PersonArgumentExceptionn'est pas aussi clair que IllegalArgumentException. Ce dernier est universellement connu pour être jeté lorsqu'un argument illégal est passé. Je m'attendrais en fait à ce que le premier soit jeté si a Personétait dans un état invalide pour un appel (similaire à InvalidOperationExceptionC #).
Selali Adobor
Il est vrai que si vous ne faites pas attention, vous pouvez déclencher la même exception ailleurs dans la fonction, mais c'est une faille du code, pas de la nature des exceptions courantes. Et c'est là que le débogage et les traces de pile entrent en jeu. Si vous essayez de "étiqueter" vos exceptions, utilisez les fonctionnalités existantes fournies par les exceptions les plus courantes, comme l'utilisation du constructeur de message (c'est exactement ce à quoi il sert). Certaines exceptions fournissent même aux constructeurs des paramètres supplémentaires pour obtenir le nom de l'argument problématique, mais vous pouvez fournir la même fonctionnalité avec un message bien formé.
Selali Adobor
J'admets que le simple changement de nom d'une exception commune à une étiquette privée de la même exception est un faible exemple et vaut rarement la peine. En pratique, j'essaie d'émettre des exceptions personnalisées à un niveau sémantique supérieur, plus intimement lié à l'intention du module.
Jonathan Eunice
J'ai effectué un travail sensible aux performances dans les domaines numérique / HPC et OS / middleware. Une augmentation des performances de 13% n'est pas une mince affaire. Je pourrais désactiver certains contrôles pour l'obtenir, de la même manière qu'un commandant militaire pourrait demander 105% de la puissance nominale du réacteur. Mais j'ai vu "cela fonctionnera plus vite de cette façon", souvent donné comme raison de désactiver les vérifications, les sauvegardes et autres protections. C'est essentiellement un compromis entre la résilience et la sécurité (dans de nombreux cas, y compris l'intégrité des données) pour des performances supplémentaires. Que cela en vaille la peine est un jugement.
Jonathan Eunice
3

Échouer dès que possible est formidable lors du débogage d'une application. Je me souviens d'une erreur de segmentation particulière dans un programme C ++ hérité: l'endroit où le bogue a été détecté n'avait rien à voir avec l'endroit où il a été introduit (le pointeur nul a été heureusement déplacé d'un endroit à un autre en mémoire avant de finalement causer un problème ). Les traces de pile ne peuvent pas vous aider dans ces cas.

Alors oui, la programmation défensive est une approche vraiment efficace pour détecter et corriger rapidement les bugs. En revanche, il peut être exagéré, notamment avec des références nulles.

Dans votre cas spécifique, par exemple: si l'une des références est nulle, le NullReferenceExceptionsera jeté à la prochaine instruction, lorsque vous tenterez d'obtenir l'âge d'une personne. Vous n'avez pas vraiment besoin de vérifier les choses par vous-même ici: laissez le système sous-jacent intercepter ces erreurs et lever des exceptions, c'est pourquoi elles existent .

Pour un exemple plus réaliste, vous pouvez utiliser des assertinstructions qui:

  1. Sont plus courts à écrire et à lire:

        assert p1 : "p1 is null";
        assert p2 : "p2 is null";
    
  2. Sont spécialement conçus pour votre approche. Dans un monde où vous avez à la fois des assertions et des exceptions, vous pouvez les distinguer comme suit:

    • Les assertions concernent les erreurs de programmation ("/ * cela ne devrait jamais arriver * /"),
    • Les exceptions concernent les cas d'angle (situations exceptionnelles mais probables)

Ainsi, exposer vos hypothèses sur les entrées et / ou l'état de votre application avec des assertions permet au développeur suivant de comprendre un peu plus le but de votre code.

Un analyseur statique (par exemple le compilateur) pourrait aussi être plus heureux.

Enfin, les assertions peuvent être supprimées de l'application déployée à l'aide d'un seul commutateur. Mais d'une manière générale, ne vous attendez pas à améliorer l'efficacité avec cela: les vérifications d'assertions à l'exécution sont négligeables.

coredump
la source
1

Autant que je sache, différents programmeurs préfèrent une solution ou l'autre.

La première solution est généralement préférée car elle est plus concise, en particulier, vous n'avez pas à vérifier encore et encore la même condition dans différentes fonctions.

Je trouve la deuxième solution, par exemple

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

plus solide, car

  1. Il intercepte l'erreur dès que possible, c'est-à-dire lorsque registerPerson() est appelé, et non lorsqu'une exception de pointeur nul est levée quelque part dans la pile des appels. Le débogage devient beaucoup plus facile: nous savons tous dans quelle mesure une valeur non valide peut parcourir le code avant de se manifester comme un bogue.
  2. Il diminue le couplage entre les fonctions: registerPerson()ne fait aucune hypothèse sur les autres fonctions qui finiront par utiliser l' personargument et comment elles vont l'utiliser: la décision qui nullest une erreur est prise et implémentée localement.

Donc, surtout si le code est assez complexe, j'ai tendance à préférer cette seconde approche.

Giorgio
la source
1
Donner la raison ("La personne saisie est nulle.") Aide également à comprendre le problème, contrairement à lorsqu'une méthode obscure dans une bibliothèque échoue.
Florian F
1

En général, oui, c'est une bonne idée d'échouer tôt. Cependant, dans votre exemple spécifique, l'explicite IllegalArgumentExceptionn'apporte pas d'amélioration significative par rapport à a NullReferenceException- car les deux objets opérés sont déjà fournis comme arguments à la fonction.

Mais regardons un exemple légèrement différent.

class PersonCalculator {
    PersonCalculator(Person p) {
        if (p == null) throw new ArgumentNullException("p");
        _p = p;
    }

    void Calculate() {
        // Do something to p
    }
}

S'il n'y avait pas d'argument archivant le constructeur, vous obtiendrez un NullReferenceExceptionlors de l'appel Calculate.

Mais le morceau de code cassé n'était ni la Calculatefonction, ni le consommateur de la Calculatefonction. Le morceau de code cassé était le code qui tente de construire le PersonCalculatoravec un null Person- c'est donc là que nous voulons que l'exception se produise.

Si nous supprimons cette vérification d'argument explicite, vous devrez comprendre pourquoi un NullReferenceExceptions'est produit lorsque le a Calculateété appelé. Et nulltrouver pourquoi l'objet a été construit avec une personne peut devenir délicat, surtout si le code qui construit la calculatrice n'est pas proche du code qui appelle réellement la Calculatefonction.

Pete
la source
0

Pas dans les exemples que vous donnez.

Comme vous le dites, lancer explicitement ne vous rapporte pas grand-chose lorsque vous allez obtenir une exception peu de temps après. Beaucoup diront qu'il est préférable d'avoir l'exception explicite avec un bon message, même si je ne suis pas d'accord. Dans les scénarios pré-publiés, la trace de la pile est suffisamment bonne. Dans les scénarios post-version, le site d'appel peut souvent fournir de meilleurs messages qu'à l'intérieur de la fonction.

Le deuxième formulaire fournit trop d'informations à la fonction. Cette fonction ne doit pas nécessairement savoir que les autres fonctions lanceront une entrée nulle. Même s'ils lancent maintenant une entrée nulle , il devient très gênant de refactoriser si cela cesse d'être le cas puisque la vérification nulle est répartie dans tout le code.

Mais en général, vous devriez lancer tôt une fois que vous avez déterminé que quelque chose s'est mal passé (tout en respectant SEC). Ce ne sont peut-être pas de bons exemples de cela.

Telastyn
la source
-3

Dans votre exemple de fonction, je préférerais que vous ne fassiez aucun contrôle et que vous NullReferenceException cela se produire.

D'une part, cela n'a aucun sens de passer un null de toute façon, donc je vais résoudre le problème immédiatement en fonction du lancement d'un NullReferenceException.

Deuxièmement, si chaque fonction génère des exceptions légèrement différentes en fonction du type d'entrées manifestement erronées fournies, alors très vite, vous avez des fonctions qui peuvent générer 18 types d'exceptions différents, et très vite vous vous dites que c'est trop beaucoup de travail à faire, et juste supprimer toutes les exceptions de toute façon.

Il n'y a rien que vous puissiez vraiment faire pour résoudre la situation d'une erreur de conception dans votre fonction, alors laissez simplement l'échec se produire sans le changer.

comment s'appelle-t-il
la source
Je travaille depuis quelques années sur une base de code basée sur ce principe: les pointeurs sont simplement dé-référencés lorsque cela est nécessaire et presque jamais vérifiés par rapport à NULL et traités explicitement. Le débogage de ce code a toujours été très long et problématique, car les exceptions (ou les vidages de mémoire) se produisent beaucoup plus tard au point où l'erreur logique se trouve. J'ai perdu littéralement des semaines à chercher certains bugs. Pour ces raisons, je préfère le style échec dès que possible, au moins pour le code complexe où il n'est pas immédiatement évident d'où provient une exception de pointeur nul.
Giorgio
"D'une part, cela n'a aucun sens de passer une valeur nulle de toute façon, donc je vais résoudre le problème immédiatement en fonction du lancement d'une exception NullReferenceException.": Pas nécessairement, comme l'illustre le deuxième exemple de Prog.
Giorgio
"Deuxièmement, si chaque fonction lève des exceptions légèrement différentes en fonction du type d'entrées manifestement erronées qui ont été fournies, alors très vite vous aurez des fonctions qui pourraient générer 18 types d'exceptions différents ...": Dans ce cas, vous peut utiliser la même exception (même NullPointerException) dans toutes les fonctions: l'important est de lever tôt, et non de lever une exception différente pour chaque fonction.
Giorgio
C'est à cela que servent les RuntimeExceptions. Toute condition qui "ne devrait pas se produire" mais empêche votre code de fonctionner devrait en générer une. Vous n'avez pas besoin de les déclarer dans la signature de la méthode. Mais vous devez les attraper à un moment donné et signaler que la fonction de l'action requestd a échoué.
Florian F
1
La question ne spécifie pas de langue, donc se fier, par exemple, à RuntimeExceptionune hypothèse non valable.