Pourquoi ne pas utiliser les exceptions comme flux de contrôle régulier?

193

Pour éviter toutes les réponses standard sur lesquelles j'aurais pu googler, je vais vous donner un exemple que vous pouvez tous attaquer à volonté.

C # et Java (et trop d'autres) ont avec beaucoup de types des comportements de «débordement» que je n'aime pas du tout (par type.MaxValue + type.SmallestValue == type.MinValueexemple:) int.MaxValue + 1 == int.MinValue.

Mais, vu ma nature vicieuse, j'ajouterai une insulte à cette blessure en étendant ce comportement à, disons, un DateTimetype Overridden . (Je sais que DateTimec'est scellé dans .NET, mais pour le bien de cet exemple, j'utilise un pseudo langage qui est exactement comme C #, sauf pour le fait que DateTime n'est pas scellé).

La Addméthode remplacée :

/// <summary>
/// Increments this date with a timespan, but loops when
/// the maximum value for datetime is exceeded.
/// </summary>
/// <param name="ts">The timespan to (try to) add</param>
/// <returns>The Date, incremented with the given timespan. 
/// If DateTime.MaxValue is exceeded, the sum wil 'overflow' and 
/// continue from DateTime.MinValue. 
/// </returns>
public DateTime override Add(TimeSpan ts) 
{
    try
    {                
        return base.Add(ts);
    }
    catch (ArgumentOutOfRangeException nb)
    {
        // calculate how much the MaxValue is exceeded
        // regular program flow
        TimeSpan saldo = ts - (base.MaxValue - this);
        return DateTime.MinValue.Add(saldo)                         
    }
    catch(Exception anyOther) 
    {
        // 'real' exception handling.
    }
}

Bien sûr, un if pourrait résoudre cela tout aussi facilement, mais le fait reste que je ne vois pas pourquoi vous ne pouvez pas utiliser d'exceptions (logiquement, c'est-à-dire que lorsque les performances sont un problème, dans certains cas, les exceptions doivent être évitées ).

Je pense que dans de nombreux cas, ils sont plus clairs que les structures if et ne rompent aucun contrat de la méthode.

À mon humble avis, la réaction «Ne jamais les utiliser pour le déroulement régulier du programme» que tout le monde semble avoir n'est pas aussi bien insuffisante que la force de cette réaction peut le justifier.

Ou est-ce que je me trompe?

J'ai lu d'autres articles traitant de toutes sortes de cas particuliers, mais mon point est qu'il n'y a rien de mal à cela si vous êtes tous les deux:

  1. Clair
  2. Respectez le contrat de votre méthode

Tirez sur moi.

Peter
la source
2
+1 Je ressens la même chose. Outre les performances, la seule bonne raison d'éviter les exceptions pour le flux de contrôle est lorsque le code de l'appelant sera beaucoup plus lisible avec les valeurs de retour.
4
est-ce que: return -1 si quelque chose s'est passé, retourne -2 si autre chose, etc ... vraiment plus lisible que les exceptions?
kender
1
Il est triste que l'on ait la réputation négative de dire la vérité: que votre exemple n'aurait pas pu être écrit avec des déclarations if. (Cela ne veut pas dire que c'est correct / complet.)
Ingo
8
Je dirais que jeter une exception pourrait parfois être votre seule option. J'ai par exemple un composant métier qui initialise son état interne au sein de son constructeur en interrogeant la base de données. Il y a des moments où aucune donnée appropriée dans la base de données n'est disponible. Lancer une exception dans le constructeur est le seul moyen d'annuler efficacement la construction de l'objet. Ceci est clairement indiqué dans le contrat (Javadoc dans mon cas) de la classe, donc je n'ai aucun problème à ce que le code client puisse (et devrait) attraper cette exception lors de la création du composant et continuer à partir de là.
Stefan Haberl
1
Puisque vous avez formulé une hypothèse, il vous incombe également de citer des preuves / raisons corroborantes. Pour commencer, nommez une des raisons pour lesquelles votre code est supérieur à une ifdéclaration beaucoup plus courte et auto-documentée . Vous trouverez cela très difficile. En d'autres termes: votre prémisse même est imparfaite, et les conclusions que vous en tirez sont donc fausses.
Konrad Rudolph

Réponses:

171

Avez-vous déjà essayé de déboguer un programme générant cinq exceptions par seconde dans le cours normal de son fonctionnement?

J'ai.

Le programme était assez complexe (c'était un serveur de calcul distribué), et une légère modification d'un côté du programme pouvait facilement casser quelque chose dans un endroit totalement différent.

J'aurais aimé pouvoir lancer le programme et attendre que des exceptions se produisent, mais il y a eu environ 200 exceptions pendant le démarrage dans le cours normal des opérations

Mon point: si vous utilisez des exceptions pour des situations normales, comment repérez-vous les situations inhabituelles (c'est-à-dire exceptionnelles )?

Bien sûr, il existe d'autres bonnes raisons de ne pas trop utiliser les exceptions, en particulier en termes de performances

Brann
la source
13
Exemple: quand je débogue un programme .net, je le lance depuis Visual Studio et je demande à VS de casser toutes les exceptions. Si vous comptez sur les exceptions comme comportement attendu, je ne peux plus le faire (car cela casserait 5 fois / s), et il est beaucoup plus compliqué de localiser la partie problématique du code.
Brann
15
+1 pour signaler que vous ne voulez pas créer une botte de foin d'exception dans laquelle trouver une aiguille exceptionnelle réelle.
Grant Wagner
16
n'obtenez pas du tout cette réponse, je pense que les gens comprennent mal ici. Cela n'a rien à voir avec le débogage du tout, mais avec la conception. C'est un raisonnement circulaire sous sa forme pure que j'ai peur. Et votre point est vraiment en dehors de la question comme indiqué précédemment
Peter
11
@Peter: Déboguer sans casser les exceptions est difficile, et attraper toutes les exceptions est douloureux s'il y en a beaucoup par conception. Je pense qu'une conception qui rend le débogage difficile est presque partiellement cassée (en d'autres termes, la conception a quelque chose à voir avec le débogage, l'OMI)
Brann
7
Même en ignorant le fait que la plupart des situations que je souhaite déboguer ne correspondent pas à des exceptions levées, la réponse à votre question est: "par type", par exemple, je vais dire à mon débogueur de n'attraper que AssertionError ou StandardError ou quelque chose qui fait correspondent à de mauvaises choses qui se passent. Si vous avez des problèmes avec cela, comment faites-vous la journalisation - ne vous connectez-vous pas par niveau et par classe, précisément pour pouvoir filtrer sur eux? Pensez-vous que c'est aussi une mauvaise idée?
Ken
158

Les exceptions sont essentiellement des gotodéclarations non locales avec toutes les conséquences de ces dernières. L'utilisation d'exceptions pour le contrôle de flux viole un principe de moindre étonnement , rendre les programmes difficiles à lire (rappelez-vous que les programmes sont d'abord écrits pour les programmeurs).

De plus, ce n'est pas ce à quoi s'attendent les éditeurs de compilateurs. Ils s'attendent à ce que des exceptions soient lancées rarement, et ils laissent généralement le throwcode être assez inefficace. Lancer des exceptions est l'une des opérations les plus coûteuses dans .NET.

Cependant, certains langages (notamment Python) utilisent des exceptions comme constructions de contrôle de flux. Par exemple, les itérateurs lèvent une StopIterationexception s'il n'y a plus d'éléments. Même les constructions de langage standard (telles que for) reposent sur cela.

Anton Gogolev
la source
11
hé, les exceptions ne sont pas étonnantes! Et vous vous contredisez un peu quand vous dites "c'est une mauvaise idée" et puis continuez en disant "mais c'est une bonne idée en python".
hasen
6
Je ne suis toujours pas convaincu du tout: 1) Effiency était en plus de la question, beaucoup de programmes non bacht s'en moquaient (par exemple, interface utilisateur) 2) Étonnant: comme je l'ai dit, c'est juste étonnant car il n'est pas utilisé, mais la question demeure: pourquoi ne pas utiliser id en premier lieu? Mais, puisque c'est la réponse
Peter
4
+1 En fait, je suis heureux que vous ayez fait la distinction entre Python et C #. Je ne pense pas que ce soit une contradiction. Python est beaucoup plus dynamique et l'attente d'utiliser des exceptions de cette manière est intégrée au langage. Cela fait également partie de la culture EAFP de Python. Je ne sais pas quelle approche est conceptuellement plus pure ou plus cohérente, mais j'aime l'idée d'écrire du code qui fait ce que les autres attendent , ce qui signifie différents styles dans différentes langues.
Mark E. Haase
13
Contrairement à goto, bien sûr, les exceptions interagissent correctement avec votre pile d'appels et avec la portée lexicale, et ne laissez pas la pile ou les portées dans le désordre.
Lukas Eder
4
En fait, la plupart des fournisseurs de VM attendent des exceptions et les gèrent efficacement. Comme @LukasEder le note, les exceptions sont totalement différentes de goto en ce qu'elles sont structurées.
Marcin
30

Ma règle de base est:

  • Si vous pouvez faire quelque chose pour récupérer d'une erreur, interceptez les exceptions
  • Si l'erreur est très courante (par exemple, l'utilisateur a tenté de se connecter avec le mauvais mot de passe), utilisez les valeurs de retour
  • Si vous ne pouvez rien faire pour récupérer d'une erreur, laissez-le non détecté (ou attrapez-le dans votre receveur principal pour faire un arrêt semi-gracieux de l'application)

Le problème que je vois avec des exceptions est d'un point de vue purement syntaxique (je suis presque sûr que la surcharge de performance est minime). Je n'aime pas les blocs d'essai partout.

Prenons cet exemple:

try
{
   DoSomeMethod();  //Can throw Exception1
   DoSomeOtherMethod();  //Can throw Exception1 and Exception2
}
catch(Exception1)
{
   //Okay something messed up, but is it SomeMethod or SomeOtherMethod?
}

.. Un autre exemple pourrait être lorsque vous avez besoin d'assigner quelque chose à un handle à l'aide d'une fabrique, et cette fabrique pourrait lever une exception:

Class1 myInstance;
try
{
   myInstance = Class1Factory.Build();
}
catch(SomeException)
{
   // Couldn't instantiate class, do something else..
}
myInstance.BestMethodEver();   // Will throw a compile-time error, saying that myInstance is uninitalized, which it potentially is.. :(

Soo, personnellement, je pense que vous devriez garder des exceptions pour les conditions d'erreur rares (mémoire insuffisante, etc.) et utiliser des valeurs de retour (classes de valeur, structs ou enums) pour faire votre vérification d'erreurs à la place.

J'espère que j'ai bien compris votre question :)

cwap
la source
4
re: Votre deuxième exemple - pourquoi ne pas simplement mettre l'appel à BestMethodEver dans le bloc try, après Build? Si Build () lève une exception, elle ne sera pas exécutée et le compilateur est content.
Blorgbeard sort le
2
Ouais, ce serait probablement ce avec quoi vous vous retrouverez, mais considérez un exemple plus complexe où le type myInstance lui-même peut lancer des exceptions .. Et d'autres anomalies dans la portée de la méthode le peuvent aussi. Vous vous retrouverez avec beaucoup de blocs try / catch imbriqués :(
cwap
Vous devez effectuer une traduction d'exception (vers un type d'exception approprié au niveau d'abstraction) dans votre bloc catch. FYI: "Multi-catch" est censé entrer dans Java 7.
jasonnerothin
FYI: En C ++, vous pouvez placer plusieurs captures après une tentative pour intercepter différentes exceptions.
RobH
2
Pour les logiciels d'emballage rétractable, vous devez détecter toutes les exceptions. Au moins mettre en place une boîte de dialogue qui explique que le programme doit s'arrêter, et voici quelque chose d'incompréhensible que vous pouvez envoyer dans un rapport de bogue.
David Thornley
25

Une première réaction à beaucoup de réponses:

vous écrivez pour les programmeurs et le principe du moindre étonnement

Bien sûr! Mais un si n'est pas plus clair tout le temps.

Cela ne devrait pas être étonnant, par exemple: divide (1 / x) catch (divisionByZero) est plus clair que tout pour moi (chez Conrad et d'autres). Le fait que ce type de programmation ne soit pas attendu est purement conventionnel et, en fait, toujours pertinent. Peut-être que dans mon exemple, un si serait plus clair.

Mais DivisionByZero et FileNotFound sont d'ailleurs plus clairs que si.

Bien sûr, s'il est moins performant et qu'il a besoin d'un million de fois par seconde, vous devriez bien sûr l'éviter, mais je n'ai toujours pas lu de bonne raison d'éviter la conception globale.

En ce qui concerne le principe du moindre étonnement: il y a ici un danger de raisonnement circulaire: supposons qu'une communauté entière utilise un mauvais design, ce design deviendra attendu! Par conséquent, le principe ne peut pas être un graal et doit être considéré avec soin.

exceptions pour les situations normales, comment repérez-vous les situations inhabituelles (c'est-à-dire exceptionnelles)?

Dans de nombreuses réactions qc. comme ça brille à travers. Attrapez-les, non? Votre méthode doit être claire, bien documentée et respectueuse de son contrat. Je ne comprends pas cette question, je dois l'admettre.

Débogage sur toutes les exceptions: pareil, cela se fait parfois car la conception de ne pas utiliser d'exceptions est courante. Ma question était: pourquoi est-ce courant en premier lieu?

Peter
la source
1
1) Vérifiez-vous toujours xavant d'appeler 1/x? 2) Enveloppez-vous chaque opération de division dans un bloc try-catch pour attraper DivideByZeroException? 3) Quelle logique mettez-vous dans le bloc catch pour récupérer DivideByZeroException?
Lightman
1
Sauf que DivisionByZero et FileNotFound sont de mauvais exemples car ce sont des cas exceptionnels qui doivent être traités comme des exceptions.
0x6C38
Il n'y a rien de "excessivement exceptionnel" dans un fichier - introuvable de la manière que les gens "anti-exception" vantent ici. openConfigFile();peut être suivi d'un FileNotFound intercepté avec { createDefaultConfigFile(); setFirstAppRun(); }une exception FileNotFound gérée correctement; pas de plantage, améliorons l'expérience de l'utilisateur final, pas pire. Vous pouvez dire "Mais que se passerait-il si ce n'était pas vraiment la première manche et qu'ils l'obtiennent à chaque fois?" Au moins, l'application s'exécute à chaque fois et ne plante pas à chaque démarrage! Sur un 1 à 10 "c'est terrible": chaque démarrage = 3 ou 4 "first-run", planter chaque démarrage = 10.
Loduwijk
Vos exemples sont des exceptions. Non, vous ne vérifiez pas toujours xavant d'appeler 1/x, car c'est généralement bien. Le cas exceptionnel est le cas où ce n'est pas bien. Nous ne parlons pas de bouleversement de la terre ici, mais par exemple pour un entier de base donné au hasard x, seulement 1 sur 4294967296 échouerait à faire la division. C'est exceptionnel et les exceptions sont un bon moyen de gérer cela. Cependant, vous pouvez utiliser des exceptions pour implémenter l'équivalent d'une switchinstruction, mais ce serait assez idiot.
Thor84no
16

Avant les exceptions, en C, il y avait setjmpet longjmpcela pouvait être utilisé pour accomplir un déroulement similaire du cadre de pile.

Ensuite, la même construction a reçu un nom: "Exception". Et la plupart des réponses s'appuient sur la signification de ce nom pour argumenter sur son utilisation, affirmant que les exceptions sont destinées à être utilisées dans des conditions exceptionnelles. Ce n'était jamais l'intention de l'original longjmp. Il y avait juste des situations où vous deviez interrompre le flux de contrôle sur de nombreux cadres de pile.

Les exceptions sont légèrement plus générales dans la mesure où vous pouvez également les utiliser dans le même cadre de pile. Cela soulève des analogies avec ce gotoque je pense être faux. Les Gotos sont une paire étroitement couplée (tout comme setjmpet longjmp). Les exceptions font suite à une publication / abonnement faiblement couplée qui est beaucoup plus propre! Par conséquent, les utiliser dans le même cadre de pile n'est pas la même chose que d'utiliser gotos.

La troisième source de confusion est de savoir s'il s'agit d'exceptions cochées ou non cochées. Bien sûr, les exceptions non contrôlées semblent particulièrement horribles à utiliser pour contrôler le flux et peut-être pour beaucoup d'autres choses.

Les exceptions cochées sont cependant idéales pour contrôler le flux, une fois que vous avez surmonté tous les problèmes victoriens et que vous vivez un peu.

Mon utilisation préférée est une séquence d' throw new Success()un long fragment de code qui essaie une chose après l'autre jusqu'à ce qu'elle trouve ce qu'elle recherche. Chaque chose - chaque élément de logique - peut avoir une imbrication arbitraire, il en breakest donc de même pour tout type de test de condition. Le if-elsemotif est cassant. Si je modifie une elsesyntaxe ou que je gâche la syntaxe d'une autre manière, il y a un bug poilu.

L'utilisation de throw new Success() linéarise le flux de code. J'utilise des Successclasses définies localement - vérifiées bien sûr - pour que si j'oublie de l'attraper, le code ne se compile pas. Et je n'attrape pas les Successes d' une autre méthode .

Parfois, mon code vérifie une chose après l'autre et ne réussit que si tout va bien. Dans ce cas, j'ai une linéarisation similaire en utilisant throw new Failure().

L'utilisation d'une fonction distincte perturbe le niveau naturel de compartimentation. La returnsolution n'est donc pas optimale. Je préfère avoir une ou deux pages de code au même endroit pour des raisons cognitives. Je ne crois pas au code ultra-finement divisé.

Ce que font les JVM ou les compilateurs est moins pertinent pour moi à moins qu'il n'y ait un hotspot. Je ne peux pas croire qu'il y ait une raison fondamentale pour les compilateurs de ne pas détecter les exceptions lancées et capturées localement et de les traiter simplement comme des gotos très efficaces au niveau du code machine.

En ce qui concerne leur utilisation dans les fonctions de contrôle du flux - c'est-à-dire pour les cas courants plutôt que pour les cas exceptionnels - je ne vois pas en quoi ils seraient moins efficaces que de multiples pannes, tests de condition, retours à parcourir trois cadres de pile au lieu de simplement restaurer le pointeur de pile.

Personnellement, je n'utilise pas le motif sur les cadres de pile et je peux voir comment il faudrait une conception sophistiquée pour le faire avec élégance. Mais utilisé avec parcimonie, ça devrait aller.

Enfin, en ce qui concerne les programmeurs vierges surprenants, ce n'est pas une raison impérieuse. Si vous les initiez doucement à la pratique, ils apprendront à l'aimer. Je me souviens que C ++ avait l'habitude de surprendre et d'effrayer les programmeurs C.

nécromancien
la source
3
En utilisant ce modèle, la plupart de mes fonctions grossières ont deux petits points à la fin - un pour le succès et un pour l'échec et c'est là que la fonction résume les choses telles que préparer la bonne réponse de servlet ou préparer les valeurs de retour. Avoir un seul endroit pour faire la synthèse, c'est bien. L' returnalternative -pattern nécessiterait deux fonctions pour chacune de ces fonctions. Un externe pour préparer la réponse de servlet ou d'autres actions similaires, et un interne pour effectuer le calcul. PS: Un professeur d'anglais suggérerait probablement que j'utilise "étonnant" plutôt que "surprenant" dans le dernier paragraphe :-)
nécromancien
11

La réponse standard est que les exceptions ne sont pas régulières et doivent être utilisées dans des cas exceptionnels.

Une raison, qui est importante pour moi, est que lorsque je lis une try-catchstructure de contrôle dans un logiciel que je maintiens ou débogue, j'essaie de savoir pourquoi le codeur d'origine a utilisé une gestion des exceptions au lieu d'une if-elsestructure. Et j'espère trouver une bonne réponse.

N'oubliez pas que vous écrivez du code non seulement pour l'ordinateur, mais également pour d'autres codeurs. Il existe une sémantique associée à un gestionnaire d'exceptions que vous ne pouvez pas jeter simplement parce que la machine ne s'en soucie pas.

mouviciel
la source
Une réponse sous-estimée je pense. L'ordinateur ne ralentit peut-être pas beaucoup lorsqu'il trouve une exception en train d'être avalée, mais lorsque je travaille sur le code de quelqu'un d'autre et que je le rencontre, cela m'arrête net pendant que je m'entraîne si j'ai raté quelque chose d'important que je n'ai pas fait Je ne sais pas, ou s'il n'y a en fait aucune justification pour l'utilisation de cet anti-modèle.
Tim Abell
9

Et les performances? Lors du test de charge d'une application Web .NET, nous avons dépassé 100 utilisateurs simulés par serveur Web jusqu'à ce que nous corrigions une exception courante et que ce nombre soit passé à 500 utilisateurs.

James Koch
la source
8

Josh Bloch traite ce sujet en détail dans Effective Java. Ses suggestions sont éclairantes et devraient également s'appliquer à .Net (sauf pour les détails).

En particulier, des exceptions devraient être utilisées pour des circonstances exceptionnelles. Les raisons en sont principalement liées à la convivialité. Pour qu'une méthode donnée soit utilisable au maximum, ses conditions d'entrée et de sortie doivent être contraintes au maximum.

Par exemple, la deuxième méthode est plus facile à utiliser que la première:

/**
 * Adds two positive numbers.
 *
 * @param addend1 greater than zero
 * @param addend2 greater than zero
 * @throws AdditionException if addend1 or addend2 is less than or equal to zero
 */
int addPositiveNumbers(int addend1, int addend2) throws AdditionException{
  if( addend1 <= 0 ){
     throw new AdditionException("addend1 is <= 0");
  }
  else if( addend2 <= 0 ){
     throw new AdditionException("addend2 is <= 0");
  }
  return addend1 + addend2;
}

/**
 * Adds two positive numbers.
 *
 * @param addend1 greater than zero
 * @param addend2 greater than zero
 */
public int addPositiveNumbers(int addend1, int addend2) {
  if( addend1 <= 0 ){
     throw new IllegalArgumentException("addend1 is <= 0");
  }
  else if( addend2 <= 0 ){
     throw new IllegalArgumentException("addend2 is <= 0");
  }
  return addend1 + addend2;
}

Dans les deux cas, vous devez vous assurer que l'appelant utilise correctement votre API. Mais dans le second cas, vous en avez besoin (implicitement). Les exceptions logicielles seront toujours levées si l'utilisateur n'a pas lu le javadoc, mais:

  1. Vous n'avez pas besoin de le documenter.
  2. Vous n'avez pas besoin de le tester (en fonction de l'agressivité de votre stratégie de test unitaire).
  3. Vous n'avez pas besoin que l'appelant gère trois cas d'utilisation.

Le point fondamental est que les exceptions ne doivent pas être utilisées comme codes de retour, en grande partie parce que vous avez compliqué non seulement VOTRE API, mais aussi l'API de l'appelant.

Faire ce qu'il faut a un coût, bien sûr. Le coût est que chacun doit comprendre qu'il doit lire et suivre la documentation. Espérons que ce soit le cas de toute façon.

Jasonnerothin
la source
7

Je pense que vous pouvez utiliser des exceptions pour le contrôle de flux. Il y a cependant un revers à cette technique. La création d'exceptions est une chose coûteuse, car elles doivent créer une trace de pile. Donc, si vous souhaitez utiliser les exceptions plus souvent que pour simplement signaler une situation exceptionnelle, vous devez vous assurer que la création des traces de pile n'influence pas négativement vos performances.

La meilleure façon de réduire le coût de création d'exceptions est de remplacer la méthode fillInStackTrace () comme ceci:

public Throwable fillInStackTrace() { return this; }

Une telle exception n'aura aucune trace de pile remplie.

paweloque
la source
Le stacktrace demande également à l'appelant de "connaître" (c'est-à-dire d'avoir une dépendance sur) tous les Throwables de la pile. C'est une mauvaise chose. Lancez des exceptions appropriées au niveau d'abstraction (ServiceExceptions dans les services, DaoExceptions des méthodes Dao, etc.). Traduisez simplement si nécessaire.
jasonnerothin
5

Je ne vois pas vraiment comment vous contrôlez le déroulement du programme dans le code que vous avez cité. Vous ne verrez jamais une autre exception en plus de l'exception ArgumentOutOfRange. (Ainsi, votre deuxième clause catch ne sera jamais atteinte). Tout ce que vous faites est d'utiliser un lancer extrêmement coûteux pour imiter une instruction if.

De plus, vous n'effectuez pas les opérations les plus sinistres où vous lancez simplement une exception uniquement pour qu'elle soit interceptée ailleurs pour effectuer le contrôle de flux. Vous gérez en fait un cas exceptionnel.

Jason Punyon
la source
5

Voici les meilleures pratiques que j'ai décrites dans mon article de blog :

  • Lancez une exception pour signaler une situation inattendue dans votre logiciel.
  • Utilisez les valeurs de retour pour la validation d'entrée .
  • Si vous savez comment gérer les exceptions qu'une bibliothèque lance, attrapez-les au niveau le plus bas possible .
  • Si vous rencontrez une exception inattendue, supprimez complètement l'opération en cours. Ne prétendez pas savoir comment y faire face .
Vladimir
la source
4

Parce que le code est difficile à lire, vous pouvez avoir des difficultés à le déboguer, vous introduirez de nouveaux bogues lors de la correction des bogues après une longue période, c'est plus cher en termes de ressources et de temps, et cela vous ennuie si vous déboguez votre code et le débogueur s'arrête à l'apparition de chaque exception;)

Gambrinus
la source
4

Outre les raisons indiquées, une des raisons de ne pas utiliser d'exceptions pour le contrôle de flux est que cela peut considérablement compliquer le processus de débogage.

Par exemple, lorsque j'essaie de localiser un bogue dans VS, j'active généralement "interrompre toutes les exceptions". Si vous utilisez des exceptions pour le contrôle de flux, je vais régulièrement interrompre le débogueur et je devrai continuer à ignorer ces exceptions non exceptionnelles jusqu'à ce que j'arrive au vrai problème. Cela risque de rendre quelqu'un fou !!

Sean
la source
1
J'ai déjà manqué ce point fort: le débogage sur toutes les exceptions: le même, c'est juste fait parce que la conception de ne pas utiliser d'exceptions est courante. Ma question était: pourquoi est-ce courant en premier lieu?
Peter
Donc, votre réponse est-elle essentiellement "C'est mauvais parce que Visual Studio a cette fonctionnalité ..."? Je programme depuis environ 20 ans maintenant, et je n'ai même pas remarqué qu'il y avait une option "pause sur toutes les exceptions". Pourtant, "à cause de cette fonctionnalité!" sonne comme une raison faible. Tracez simplement l'exception jusqu'à sa source; j'espère que vous utilisez un langage qui rend cela facile - sinon votre problème est avec les fonctionnalités du langage, pas avec l'utilisation générale des exceptions elles-mêmes.
Loduwijk
3

Supposons que vous ayez une méthode qui effectue certains calculs. Il y a de nombreux paramètres d'entrée qu'il doit valider, puis renvoyer un nombre supérieur à 0.

En utilisant des valeurs de retour pour signaler une erreur de validation, c'est simple: si la méthode retournait un nombre inférieur à 0, une erreur se produisait. Comment savoir alors quel paramètre n'a pas validé?

Je me souviens de mes jours C, beaucoup de fonctions renvoyaient des codes d'erreur comme celui-ci:

-1 - x lesser then MinX
-2 - x greater then MaxX
-3 - y lesser then MinY

etc.

Est-ce vraiment moins lisible que de lancer et d'attraper une exception?

kender
la source
c'est pourquoi ils ont inventé les énumérations :) Mais les nombres magiques sont un sujet complètement différent .. en.wikipedia.org/wiki
Isak Savo
Excellent exemple. J'étais sur le point d'écrire la même chose. @IsakSavo: Les énumérations ne sont pas utiles dans cette situation si la méthode est censée renvoyer une valeur ou un objet de signification. Par exemple, getAccountBalance () doit renvoyer un objet Money, pas un objet AccountBalanceResultEnum. De nombreux programmes C ont un modèle similaire où une valeur sentinelle (0 ou null) représente une erreur, puis vous devez appeler une autre fonction pour obtenir un code d'erreur distinct afin de déterminer pourquoi l'erreur s'est produite. (L'API C MySQL est comme ça.)
Mark E. Haase
3

Vous pouvez utiliser la griffe d'un marteau pour tourner une vis, tout comme vous pouvez utiliser des exceptions pour contrôler le flux. Cela ne signifie pas que c'est l' utilisation prévue de la fonctionnalité. Les ifétats de conditions, dont l' utilisation Exprime destinée est contrôler le débit.

Si vous utilisez une fonctionnalité de manière non intentionnelle tout en choisissant de ne pas utiliser la fonctionnalité conçue à cet effet, il y aura un coût associé. Dans ce cas, la clarté et les performances ne souffrent d'aucune réelle valeur ajoutée. Qu'est-ce que l'utilisation d'exceptions vous achète par rapport à la ifdéclaration largement acceptée ?

Dit autrement: ce n'est pas parce que vous pouvez que vous devriez le faire .

Bryan Watts
la source
1
Êtes-vous en train de dire qu'il n'y a pas besoin d'exception, après que nous ayons ifune utilisation normale ou l'utilisation d'exceptions n'est pas intentionnelle parce qu'elle n'est pas intentionnelle (argument circulaire)?
Val du
1
@Val: Les exceptions sont pour des situations exceptionnelles - si nous en détectons suffisamment pour lancer une exception et la gérer, nous avons suffisamment d'informations pour ne pas la lancer et continuer à la gérer. Nous pouvons passer directement à la logique de manipulation et sauter le try / catch coûteux et superflu.
Bryan Watts
Selon cette logique, vous pourriez aussi bien ne pas avoir d'exceptions et toujours faire une sortie système au lieu de lever une exception. Si vous voulez faire quelque chose avant de quitter, créez un wrapper et appelez-le. Exemple Java: public class ExitHelper{ public static void cleanExit() { cleanup(); System.exit(1); } }Alors appelez simplement cela au lieu de lancer: ExitHelper.cleanExit();Si votre argument était valable, alors ce serait l'approche préférée et il n'y aurait pas d'exceptions. Vous dites essentiellement "La seule raison des exceptions est de planter d'une manière différente".
Loduwijk
@Aaron: Si je lance et attrape à la fois l'exception, j'ai suffisamment d'informations pour éviter de le faire. Cela ne veut pas dire que toutes les exceptions sont soudainement fatales. Un autre code que je ne contrôle pas pourrait l'attraper, et c'est très bien. Mon argument, qui reste valable, est que lancer et attraper une exception dans le même contexte est superflu. Je n’ai pas dit non plus que toutes les exceptions devraient mettre fin au processus.
Bryan Watts
@BryanWatts reconnu. Beaucoup d'autres ont dit que vous ne devriez utiliser des exceptions que pour tout ce dont vous ne pouvez pas récupérer et que, par extension, vous devriez toujours planter sur les exceptions. C'est pourquoi il est difficile de discuter de ces choses; il n'y a pas que 2 opinions, mais beaucoup. Je suis toujours en désaccord avec vous, mais pas fortement. Il y a des moments où lancer / attraper ensemble est le code le plus lisible, maintenable et le plus beau; Cela se produit généralement si vous attrapez déjà d'autres exceptions, donc vous avez déjà try / catch et ajouter 1 ou 2 captures supplémentaires est plus propre que des vérifications d'erreurs séparées par if.
Loduwijk
3

Comme d'autres l'ont mentionné à de nombreuses reprises, le principe du moindre étonnement vous interdira d'utiliser des exceptions de manière excessive à des fins de contrôle du flux uniquement. D'un autre côté, aucune règle n'est correcte à 100%, et il y a toujours des cas où une exception est "juste le bon outil" - un peu comme gotoelle-même, d'ailleurs, qui se présente sous la forme breaket continuedans des langages comme Java, qui sont souvent le moyen idéal pour sortir de boucles fortement imbriquées, qui ne sont pas toujours évitables.

Le billet de blog suivant explique un cas d'utilisation plutôt complexe mais aussi plutôt intéressant pour un non-local ControlFlowException :

Il explique comment à l'intérieur de jOOQ (une bibliothèque d'abstraction SQL pour Java) , de telles exceptions sont parfois utilisées pour abandonner le processus de rendu SQL prématurément lorsqu'une condition "rare" est remplie.

Des exemples de telles conditions sont:

  • Trop de valeurs de liaison sont rencontrées. Certaines bases de données ne prennent pas en charge des nombres arbitraires de valeurs de liaison dans leurs instructions SQL (SQLite: 999, Ingres 10.1.0: 1024, Sybase ASE 15.5: 2000, SQL Server 2008: 2100). Dans ces cas, jOOQ interrompt la phase de rendu SQL et restitue l'instruction SQL avec des valeurs de liaison intégrées. Exemple:

    // Pseudo-code attaching a "handler" that will
    // abort query rendering once the maximum number
    // of bind values was exceeded:
    context.attachBindValueCounter();
    String sql;
    try {
    
      // In most cases, this will succeed:
      sql = query.render();
    }
    catch (ReRenderWithInlinedVariables e) {
      sql = query.renderWithInlinedBindValues();
    }

    Si nous extrayions explicitement les valeurs de liaison de la requête AST pour les compter à chaque fois, nous gaspillerions de précieux cycles de processeur pour ces 99,9% des requêtes qui ne souffrent pas de ce problème.

  • Une certaine logique n'est disponible qu'indirectement via une API que nous ne voulons exécuter que «partiellement». La UpdatableRecord.store()méthode génère une instruction INSERTor UPDATE, en fonction Recorddes indicateurs internes du. De «l'extérieur», nous ne savons pas dans quel type de logique se trouve store()(par exemple le verrouillage optimiste, la gestion de l'écouteur d'événements, etc.), nous ne voulons donc pas répéter cette logique lorsque nous stockons plusieurs enregistrements dans une instruction batch, où nous aimerions avoir store()uniquement généré l'instruction SQL, pas réellement l'exécuter. Exemple:

    // Pseudo-code attaching a "handler" that will
    // prevent query execution and throw exceptions
    // instead:
    context.attachQueryCollector();
    
    // Collect the SQL for every store operation
    for (int i = 0; i < records.length; i++) {
      try {
        records[i].store();
      }
    
      // The attached handler will result in this
      // exception being thrown rather than actually
      // storing records to the database
      catch (QueryCollectorException e) {
    
        // The exception is thrown after the rendered
        // SQL statement is available
        queries.add(e.query());                
      }
    }

    Si nous avions externalisé la store()logique en une API "réutilisable" qui peut être personnalisée pour éventuellement ne pas exécuter le SQL, nous chercherions à créer une API plutôt difficile à maintenir, difficilement réutilisable.

Conclusion

En substance, notre utilisation de ces non-locaux gotoest juste dans le sens de ce que [Mason Wheeler] [5] a dit dans sa réponse:

"Je viens de rencontrer une situation que je ne peux pas gérer correctement à ce stade, car je n'ai pas assez de contexte pour la gérer, mais la routine qui m'a appelé (ou quelque chose plus haut dans la pile d'appels) devrait savoir comment la gérer . "

Les deux utilisations de ControlFlowExceptionsétaient plutôt faciles à mettre en œuvre par rapport à leurs alternatives, ce qui nous permet de réutiliser un large éventail de logique sans la refactoriser hors des composants internes pertinents.

Mais le sentiment que cela est un peu une surprise pour les futurs responsables demeure. Le code semble plutôt délicat et bien que ce soit le bon choix dans ce cas, nous préférerions toujours ne pas utiliser d'exceptions pour le flux de contrôle local , où il est facile d'éviter d'utiliser le branchement ordinaire if - else.

Lukas Eder
la source
2

En règle générale, il n'y a rien de mal, en soi, à gérer une exception à un niveau bas. Une exception EST un message valide qui fournit beaucoup de détails sur les raisons pour lesquelles une opération ne peut pas être effectuée. Et si vous pouvez gérer cela, vous devriez le faire.

En général, si vous savez qu'il y a une forte probabilité d'échec que vous pouvez vérifier ... vous devriez faire la vérification ... c'est-à-dire si (obj! = Null) obj.method ()

Dans votre cas, je ne suis pas assez familier avec la bibliothèque C # pour savoir si la date et l'heure ont un moyen facile de vérifier si un horodatage est hors limites. Si c'est le cas, appelez simplement if (.isvalid (ts)) sinon votre code est fondamentalement correct.

Donc, fondamentalement, cela revient à la manière dont crée un code plus propre ... si l'opération de protection contre une exception attendue est plus complexe que la simple gestion de l'exception; que vous avez ma permission de gérer l'exception au lieu de créer des gardes complexes partout.


la source
Point supplémentaire: Si votre exception fournit des informations de capture des échecs (un getter comme "Param getWhatParamMessedMeUp ()"), cela peut aider l'utilisateur de votre API à prendre une bonne décision quant à ce qu'il faut faire ensuite. Sinon, vous donnez simplement un nom à un état d'erreur.
jasonnerothin
2

Si vous utilisez des gestionnaires d'exceptions pour le flux de contrôle, vous êtes trop général et paresseux. Comme quelqu'un l'a mentionné, vous savez que quelque chose s'est passé si vous gérez le traitement dans le gestionnaire, mais que se passe-t-il exactement? Vous utilisez essentiellement l'exception pour une instruction else, si vous l'utilisez pour le flux de contrôle.

Si vous ne savez pas quel état possible pourrait se produire, vous pouvez utiliser un gestionnaire d'exceptions pour les états inattendus, par exemple lorsque vous devez utiliser une bibliothèque tierce, ou vous devez tout attraper dans l'interface utilisateur pour afficher une belle erreur message et enregistrez l'exception.

Cependant, si vous savez ce qui pourrait mal tourner et que vous ne mettez pas de déclaration if ou quelque chose pour le vérifier, alors vous êtes simplement paresseux. Permettre au gestionnaire d'exceptions d'être le fourre-tout pour les choses que vous savez que cela pourrait arriver est paresseux, et il reviendra vous hanter plus tard, car vous essaierez de corriger une situation dans votre gestionnaire d'exceptions sur la base d'une hypothèse éventuellement fausse.

Si vous mettez de la logique dans votre gestionnaire d'exceptions pour déterminer ce qui s'est exactement passé, vous seriez assez stupide de ne pas mettre cette logique dans le bloc try.

Les gestionnaires d'exceptions sont le dernier recours, lorsque vous manquez d'idées / de moyens pour empêcher quelque chose de mal tourner ou que les choses sont hors de votre capacité à contrôler. Par exemple, le serveur est en panne et expire et vous ne pouvez pas empêcher cette exception d'être levée.

Enfin, avoir toutes les vérifications effectuées à l'avance montre ce que vous savez ou prévoyez se produira et le rend explicite. Le code doit être clair dans l'intention. Que préférez-vous lire?


la source
1
Pas vrai du tout: "Vous utilisez essentiellement l'exception pour une instruction else, si vous l'utilisez pour le flux de contrôle." Si vous l'utilisez pour le flux de contrôle, vous savez exactement ce que vous attrapez et n'utilisez jamais une capture générale, mais un spécifique bien sûr!
Peter
2

Vous pourriez être intéressé à jeter un œil au système de conditions de Common Lisp qui est une sorte de généralisation des exceptions bien faites. Parce que vous pouvez dérouler la pile ou non de manière contrôlée, vous obtenez également des "redémarrages", qui sont extrêmement pratiques.

Cela n'a rien à voir avec les meilleures pratiques dans d'autres langues, mais cela vous montre ce qui peut être fait avec une certaine conception pensée dans (à peu près) la direction à laquelle vous pensez.

Bien sûr, il y a encore des considérations de performances si vous rebondissez de haut en bas dans la pile comme un yo-yo, mais c'est une idée beaucoup plus générale que l'approche «oh merde, laisse tomber» que la plupart des systèmes d'exception catch / throw incarnent.

Simon
la source
2

Je ne pense pas qu'il y ait quelque chose de mal à utiliser des exceptions pour le contrôle de flux. Les exceptions sont quelque peu similaires aux continuations et dans les langages à typage statique, les exceptions sont plus puissantes que les continuations, donc, si vous avez besoin de continuations mais que votre langage ne les a pas, vous pouvez utiliser des exceptions pour les implémenter.

Eh bien, en fait, si vous avez besoin de suites et que votre langue ne les a pas, vous avez choisi la mauvaise langue et vous devriez plutôt en utiliser une autre. Mais parfois, vous n'avez pas le choix: la programmation Web côté client est le meilleur exemple - il n'y a tout simplement aucun moyen de contourner JavaScript.

Un exemple: Microsoft Volta est un projet permettant d'écrire des applications Web dans .NET simple et de laisser le framework se charger de déterminer quels bits doivent s'exécuter où. Une conséquence de ceci est que Volta doit être capable de compiler CIL en JavaScript, afin que vous puissiez exécuter du code sur le client. Cependant, il y a un problème: .NET a le multithreading, pas JavaScript. Ainsi, Volta implémente des continuations en JavaScript à l'aide d'exceptions JavaScript, puis implémente les threads .NET en utilisant ces continuations. De cette façon, les applications Volta qui utilisent des threads peuvent être compilées pour s'exécuter dans un navigateur non modifié - aucun Silverlight n'est nécessaire.

Jörg W Mittag
la source
2

Une raison esthétique:

Un essai est toujours accompagné d'un crochet, alors qu'un if ne doit pas nécessairement être accompagné d'un else.

if (PerformCheckSucceeded())
   DoSomething();

Avec try / catch, cela devient beaucoup plus verbeux.

try
{
   PerformCheckSucceeded();
   DoSomething();
}
catch
{
}

Cela fait 6 lignes de code de trop.

gzak
la source
1

Mais vous ne saurez pas toujours ce qui se passe dans la ou les méthodes que vous appelez. Vous ne saurez pas exactement où l'exception a été lancée. Sans examiner l'objet d'exception plus en détail ...

Engrener
la source
1

Je pense qu'il n'y a rien de mal avec votre exemple. Au contraire, ce serait un péché d'ignorer l'exception lancée par la fonction appelée.

Dans la JVM, lancer une exception n'est pas si cher, ne créer que l'exception avec la nouvelle xyzException (...), car cette dernière implique un parcours de pile. Donc, si vous avez créé des exceptions à l'avance, vous pouvez les lancer plusieurs fois sans frais. Bien sûr, de cette façon, vous ne pouvez pas transmettre de données avec l'exception, mais je pense que c'est une mauvaise chose à faire de toute façon.

Ingo
la source
Désolé, c'est tout simplement faux, Brann. Cela dépend de la condition. Ce n'est pas toujours le cas que la condition soit triviale. Ainsi, une déclaration if peut prendre des heures, des jours ou même plus.
Ingo
Dans la JVM, c'est. Pas plus cher qu'un retour. Allez comprendre. Mais la question est, qu'écririez-vous dans l'instruction if, sinon le code même qui est déjà présent dans la fonction appelée pour distinguer un cas exceptionnel d'un cas normal - donc duplication de code.
Ingo
1
Ingo: une situation exceptionnelle est celle à laquelle on ne s'attend pas. c'est-à-dire que vous n'avez pas pensé en tant que programmeur. Donc ma règle est "écrire du code qui ne lève pas d'exceptions" :)
Brann
1
Je n'écris jamais de gestionnaires d'exceptions, je résout toujours le problème (sauf quand je ne peux pas le faire parce que je n'ai pas le contrôle sur le code défectueux). Et je ne lance jamais d'exceptions, sauf lorsque le code que j'ai écrit est destiné à être utilisé par quelqu'un d'autre (par exemple une bibliothèque). Non montre-moi la contradiction?
Brann
1
Je suis d'accord avec vous pour ne pas jeter d'exceptions à la sauvagerie. Mais certainement, ce qui est «exceptionnel» est une question de définition. Ce String.parseDouble lève une exception s'il ne peut pas fournir un résultat utile, par exemple, est dans l'ordre. Que devrait-il faire d'autre? Retourner NaN? Qu'en est-il du matériel non IEEE?
Ingo
1

Il existe quelques mécanismes généraux par lesquels un langage pourrait permettre à une méthode de quitter sans renvoyer une valeur et de se dérouler au bloc "catch" suivant:

  • Demandez à la méthode d'examiner le cadre de la pile pour déterminer le site d'appel et d'utiliser les métadonnées du site d'appel pour trouver soit des informations sur un trybloc dans la méthode appelante, soit l'emplacement où la méthode appelante a stocké l'adresse de son appelant; dans ce dernier cas, examinez les métadonnées que l'appelant de l'appelant doit déterminer de la même manière que l'appelant immédiat, en répétant jusqu'à ce que l'on trouve un trybloc ou que la pile soit vide. Cette approche ajoute très peu de surcharge au cas sans exception (elle exclut certaines optimisations) mais est coûteuse lorsqu'une exception se produit.

  • Demandez à la méthode de renvoyer un indicateur "caché" qui distingue un retour normal d'une exception, et demandez à l'appelant de vérifier cet indicateur et de se connecter à une routine "d'exception" si elle est définie. Cette routine ajoute 1 à 2 instructions au cas sans exception, mais relativement peu de temps système lorsqu'une exception se produit.

  • Demandez à l'appelant de placer les informations ou le code de gestion des exceptions à une adresse fixe par rapport à l'adresse de retour empilée. Par exemple, avec l'ARM, au lieu d'utiliser l'instruction "BL subroutine", on pourrait utiliser la séquence:

        adr lr,next_instr
        b subroutine
        b handle_exception
    next_instr:
    

Pour quitter normalement, le sous-programme ferait simplement bx lrou pop {pc}; en cas de sortie anormale, le sous-programme soustraire 4 à LR avant d'effectuer le retour ou d'utiliser sub lr,#4,pc(selon la variation ARM, le mode d'exécution, etc.) Cette approche ne fonctionnera pas très mal si l'appelant n'est pas conçu pour l'accueillir.

Un langage ou un cadre qui utilise des exceptions vérifiées pourrait bénéficier de la gestion de celles-ci avec un mécanisme comme # 2 ou # 3 ci-dessus, tandis que les exceptions non vérifiées sont gérées en utilisant # 1. Bien que la mise en œuvre d'exceptions vérifiées en Java soit plutôt gênante, ce ne serait pas un mauvais concept s'il y avait un moyen par lequel un site d'appel pourrait dire, essentiellement, "Cette méthode est déclarée comme lançant XX, mais je ne m'y attend pas jamais à le faire; si c'est le cas, renvoyer comme une exception "non vérifiée". Dans un cadre où les exceptions vérifiées étaient gérées de cette manière, elles pourraient être un moyen efficace de contrôle de flux pour des choses comme l'analyse des méthodes qui, dans certains contextes, peuvent avoir un forte probabilité d'échec, mais où l'échec devrait renvoyer des informations fondamentalement différentes de celles du succès. Cependant, je ne connais aucun framework utilisant un tel modèle. Au lieu de cela, le modèle le plus courant consiste à utiliser la première approche ci-dessus (coût minimal pour le cas sans exception, mais coût élevé lorsque des exceptions sont levées) pour toutes les exceptions.

supercat
la source