Lequel, et pourquoi, préférez-vous les exceptions ou les codes de retour?

92

Ma question est de savoir ce que la plupart des développeurs préfèrent pour la gestion des erreurs, les exceptions ou les codes de retour d'erreur. Veuillez être spécifique à la langue (ou à la famille de langues) et expliquer pourquoi vous préférez l'une à l'autre.

Je demande cela par curiosité. Personnellement, je préfère les codes de retour d'erreur car ils sont moins explosifs et ne forcent pas le code utilisateur à payer la pénalité de performance d'exception s'ils ne le souhaitent pas.

mise à jour: merci pour toutes les réponses! Je dois dire que même si je n'aime pas l'imprévisibilité du flux de code avec des exceptions. La réponse sur le code de retour (et leurs poignées de frère aîné) ajoute beaucoup de bruit au code.

Robert Gould
la source
1
Un problème de poule ou d'œuf du monde du logiciel ... éternellement discutable. :)
Gishu
Désolé, mais j'espère qu'avoir une variété d'opinions aidera les gens (moi y compris) à choisir de manière appropriée.
Robert Gould

Réponses:

107

Pour certains langages (c'est-à-dire C ++), la fuite de ressources ne devrait pas être une raison

C ++ est basé sur RAII.

Si vous avez du code qui pourrait échouer, retourner ou lancer (c'est-à-dire la plupart du code normal), alors vous devriez avoir votre pointeur enveloppé dans un pointeur intelligent (en supposant que vous avez une très bonne raison de ne pas créer votre objet sur la pile).

Les codes de retour sont plus détaillés

Ils sont verbeux et ont tendance à se développer en quelque chose comme:

if(doSomething())
{
   if(doSomethingElse())
   {
      if(doSomethingElseAgain())
      {
          // etc.
      }
      else
      {
         // react to failure of doSomethingElseAgain
      }
   }
   else
   {
      // react to failure of doSomethingElse
   }
}
else
{
   // react to failure of doSomething
}

Au final, votre code est une collection d'instructions identifiées (j'ai vu ce genre de code dans le code de production).

Ce code pourrait bien être traduit en:

try
{
   doSomething() ;
   doSomethingElse() ;
   doSomethingElseAgain() ;
}
catch(const SomethingException & e)
{
   // react to failure of doSomething
}
catch(const SomethingElseException & e)
{
   // react to failure of doSomethingElse
}
catch(const SomethingElseAgainException & e)
{
   // react to failure of doSomethingElseAgain
}

Quel code distinct et traitement des erreurs, ce qui peut être une bonne chose.

Les codes de retour sont plus fragiles

Si ce n'est pas un avertissement obscur d'un compilateur (voir le commentaire de "phjr"), ils peuvent facilement être ignorés.

Avec les exemples ci-dessus, supposons que quelqu'un oublie de gérer son erreur possible (cela se produit ...). L'erreur est ignorée lorsqu'elle est "retournée", et risque d'exploser plus tard (c'est-à-dire un pointeur NULL). Le même problème ne se produira pas avec exception.

L'erreur ne sera pas ignorée. Parfois, vous voulez qu'elle n'explose pas, cependant ... Vous devez donc choisir avec soin.

Les codes de retour doivent parfois être traduits

Disons que nous avons les fonctions suivantes:

  • doSomething, qui peut renvoyer un int appelé NOT_FOUND_ERROR
  • doSomethingElse, qui peut renvoyer un booléen "false" (en cas d'échec)
  • doSomethingElseAgain, qui peut renvoyer un objet Error (avec à la fois les variables __LINE__, __FILE__ et la moitié des variables de pile.
  • doTryToDoSomethingWithAllThisMess qui, eh bien ... Utilisez les fonctions ci-dessus, et retournez un code d'erreur de type ...

Quel est le type de retour de doTryToDoSomethingWithAllThisMess si l'une de ses fonctions appelées échoue?

Les codes de retour ne sont pas une solution universelle

Les opérateurs ne peuvent pas renvoyer un code d'erreur. Les constructeurs C ++ ne le peuvent pas non plus.

Les codes de retour signifient que vous ne pouvez pas enchaîner les expressions

Le corollaire du point ci-dessus. Et si je veux écrire:

CMyType o = add(a, multiply(b, c)) ;

Je ne peux pas, car la valeur de retour est déjà utilisée (et parfois, elle ne peut pas être modifiée). La valeur de retour devient donc le premier paramètre, envoyé comme référence ... Ou pas.

Les exceptions sont saisies

Vous pouvez envoyer différentes classes pour chaque type d'exception. Les exceptions de ressources (c'est-à-dire en mémoire) devraient être légères, mais tout le reste pourrait être aussi lourd que nécessaire (j'aime l'exception Java qui me donne toute la pile).

Chaque prise peut alors être spécialisée.

Ne jamais utiliser la capture (...) sans relancer

En règle générale, vous ne devez pas masquer une erreur. Si vous ne relancez pas, à tout le moins, enregistrez l'erreur dans un fichier, ouvrez une boîte de message, peu importe ...

Les exceptions sont ... NUKE

Le problème avec l'exception est que leur utilisation excessive produira du code plein d'essais / captures. Mais le problème est ailleurs: qui essaie / capture son code en utilisant le conteneur STL? Pourtant, ces conteneurs peuvent envoyer une exception.

Bien sûr, en C ++, ne laissez jamais une exception quitter un destructeur.

Les exceptions sont ... synchrones

Assurez-vous de les attraper avant qu'ils ne mettent votre thread à genoux ou ne se propagent à l'intérieur de votre boucle de messages Windows.

La solution pourrait être de les mélanger?

Donc je suppose que la solution est de jeter quand quelque chose ne devrait pas arriver. Et quand quelque chose peut arriver, utilisez un code de retour ou un paramètre pour permettre à l'utilisateur d'y réagir.

Donc, la seule question est "qu'est-ce que quelque chose qui ne devrait pas arriver?"

Cela dépend du contrat de votre fonction. Si la fonction accepte un pointeur, mais spécifie que le pointeur doit être non-NULL, alors il est correct de lever une exception lorsque l'utilisateur envoie un pointeur NULL (la question étant, en C ++, quand l'auteur de la fonction n'a-t-il pas utilisé des références à la place de pointeurs, mais ...)

Une autre solution serait d'afficher l'erreur

Parfois, votre problème est que vous ne voulez pas d'erreurs. Utiliser des exceptions ou des codes de retour d'erreur est cool, mais ... vous voulez en savoir plus.

Dans mon travail, nous utilisons une sorte de "Assert". Cela dépendra des valeurs d'un fichier de configuration, quelles que soient les options de compilation de débogage / publication:

  • consigner l'erreur
  • ouvrir une boîte de message avec un "Hey, vous avez un problème"
  • ouvrir une boîte de message avec un "Hey, vous avez un problème, voulez-vous déboguer"

Dans le développement et les tests, cela permet à l'utilisateur de localiser le problème exactement quand il est détecté, et non après (lorsque certains codes se soucient de la valeur de retour, ou à l'intérieur d'une capture).

Il est facile d'ajouter au code hérité. Par exemple:

void doSomething(CMyObject * p, int iRandomData)
{
   // etc.
}

conduit une sorte de code similaire à:

void doSomething(CMyObject * p, int iRandomData)
{
   if(iRandomData < 32)
   {
      MY_RAISE_ERROR("Hey, iRandomData " << iRandomData << " is lesser than 32. Aborting processing") ;
      return ;
   }

   if(p == NULL)
   {
      MY_RAISE_ERROR("Hey, p is NULL !\niRandomData is equal to " << iRandomData << ". Will throw.") ;
      throw std::some_exception() ;
   }

   if(! p.is Ok())
   {
      MY_RAISE_ERROR("Hey, p is NOT Ok!\np is equal to " << p->toString() << ". Will try to continue anyway") ;
   }

   // etc.
}

(J'ai des macros similaires qui ne sont actives que sur le débogage).

Notez qu'en production, le fichier de configuration n'existe pas, donc le client ne voit jamais le résultat de cette macro ... Mais il est facile de l'activer en cas de besoin.

Conclusion

Lorsque vous codez à l'aide de codes de retour, vous vous préparez à l'échec et espérez que votre forteresse de tests est suffisamment sécurisée.

Lorsque vous codez à l'aide d'une exception, vous savez que votre code peut échouer et placer généralement la capture de contre-feu à la position stratégique choisie dans votre code. Mais généralement, votre code concerne plus "ce qu'il doit faire" que "ce que je crains qu'il se passe".

Mais quand vous codez, vous devez utiliser le meilleur outil à votre disposition, et parfois, c'est "Ne cachez jamais une erreur, et montrez-la le plus tôt possible". La macro dont j'ai parlé ci-dessus suit cette philosophie.

Paercebal
la source
3
Ouais, MAIS en ce qui concerne votre premier exemple, cela pourrait facilement s'écrire: if( !doSomething() ) { puts( "ERROR - doSomething failed" ) ; return ; // or react to failure of doSomething } if( !doSomethingElse() ) { // react to failure of doSomethingElse() }
bobobobo
Pourquoi pas ... Mais alors, je trouve toujours doSomething(); doSomethingElse(); ...mieux car si j'ai besoin d'ajouter if / while / etc. à des fins d'exécution normales, je ne veux pas qu'elles soient mélangées avec if / while / etc. instructions ajoutées à des fins exceptionnelles ... Et comme la vraie règle concernant l'utilisation des exceptions est de lancer , et non d'attraper , les instructions try / catch ne sont généralement pas invasives.
paercebal
1
Votre premier point montre quel est le problème des exceptions. Votre flux de contrôle devient bizarre et est séparé du problème réel. Il remplace certains niveaux d'identification par une cascade de captures. J'utiliserais les deux, des codes de retour (ou des objets de retour avec des informations lourdes) pour d'éventuelles erreurs et des exceptions pour des choses qui ne sont pas attendues .
Peter
2
@Peter Weber: Ce n'est pas bizarre. Il est séparé du problème réel car il ne fait pas partie du flux d'exécution normal . C'est une exécution exceptionnelle . Et puis, encore une fois, le point sur l'exception est, en cas d' erreur exceptionnelle , de lancer souvent , et d' attraper rarement , voire jamais. Ainsi, les blocs catch apparaissent rarement même dans le code.
paercebal
L'exemple de ce genre de débat est très simpliste. Habituellement, "doSomething ()" ou "doSomethingElse ()" effectuent en fait quelque chose, comme changer l'état d'un objet. Le code d'exception ne garantit pas le retour de l'objet à l'état précédent et encore moins lorsque le catch est très loin du lancer ... Par exemple, imaginez que doSomething est appelé deux fois, et incrémentez un compteur avant de lancer. Comment savez-vous lors de la capture de l'exception que vous devez décrémenter une ou deux fois? En général, écrire un code de sécurité d'exception pour tout ce qui n'est pas un exemple de jouet est très difficile (impossible?).
xryl669
36

J'utilise les deux en fait.

J'utilise des codes de retour s'il s'agit d'une erreur connue et possible. Si c'est un scénario dont je sais qu'il peut se produire et se produira, alors il y a un code qui est renvoyé.

Les exceptions sont utilisées uniquement pour des choses auxquelles je ne m'attends PAS.

Stephen Wrighton
la source
+1 pour un chemin très simple. Au moins C'est assez court pour que je puisse le lire très rapidement. :-)
Ashish Gupta
1
"Les exceptions sont utilisées uniquement pour des choses auxquelles je ne m'attends PAS. " Si vous ne les attendez pas, pourquoi les utiliser ou comment les utiliser?
anar khalilov
5
Ce n'est pas parce que je ne m'attends pas à ce que cela se produise que je ne vois pas comment cela pourrait. Je m'attends à ce que mon serveur SQL soit activé et qu'il réponde. Mais je continue de coder mes attentes afin de pouvoir échouer correctement si un temps d'arrêt inattendu se produit.
Stephen Wrighton
Un serveur SQL qui ne répond pas ne serait-il pas facilement classé sous «erreur connue et possible»?
tkburbidge
21

Selon le chapitre 7 intitulé «Exceptions» dans Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries , de nombreuses justifications sont données pour expliquer pourquoi l'utilisation d'exceptions sur les valeurs de retour est nécessaire pour les frameworks OO tels que C #.

C'est peut-être la raison la plus convaincante (page 179):

"Les exceptions s'intègrent bien avec les langages orientés objet. Les langages orientés objet ont tendance à imposer des contraintes sur les signatures des membres qui ne sont pas imposées par des fonctions dans des langages non OO. Par exemple, dans le cas des constructeurs, des surcharges d'opérateurs et des propriétés, le développeur n'a pas le choix dans la valeur de retour. Pour cette raison, il n'est pas possible de normaliser les rapports d'erreurs basés sur la valeur de retour pour les frameworks orientés objet. Une méthode de rapport d'erreurs, telle que les exceptions, qui est hors de la bande de signature est la seule option. "

hâte
la source
10

Ma préférence (en C ++ et Python) est d'utiliser des exceptions. Les fonctionnalités fournies par le langage en font un processus bien défini pour à la fois lever, attraper et (si nécessaire) relancer des exceptions, ce qui rend le modèle facile à voir et à utiliser. Conceptuellement, c'est plus propre que les codes de retour, en ce sens que des exceptions spécifiques peuvent être définies par leurs noms et être accompagnées d'informations supplémentaires. Avec un code de retour, vous êtes limité à la valeur d'erreur (à moins que vous ne souhaitiez définir un objet ReturnStatus ou quelque chose).

À moins que le code que vous écrivez ne soit critique en termes de temps, la surcharge associée au déroulement de la pile n'est pas suffisamment importante pour être préoccupée.

Jason Etheridge
la source
2
N'oubliez pas que l'utilisation d'exceptions rend l'analyse des programmes plus difficile.
Paweł Hajdan
7

Les exceptions ne doivent être renvoyées que lorsque quelque chose se produit auquel vous ne vous attendiez pas.

L'autre point d'exceptions, historiquement, est que les codes de retour sont intrinsèquement propriétaires, parfois un 0 pourrait être retourné d'une fonction C pour indiquer le succès, parfois -1, ou l'un d'eux pour un échec avec 1 pour un succès. Même lorsqu'elles sont énumérées, les énumérations peuvent être ambiguës.

Les exceptions peuvent également fournir beaucoup plus d'informations, et préciser spécifiquement `` Quelque chose s'est mal passé, voici quoi, une trace de pile et des informations de support pour le contexte ''

Cela étant dit, un code de retour bien énuméré peut être utile pour un ensemble connu de résultats, un simple `` voici les résultats de la fonction, et il s'est simplement déroulé de cette façon ''

johnc
la source
6

En Java, j'utilise (dans l'ordre suivant):

  1. Conception par contrat (s'assurer que les conditions préalables sont remplies avant d'essayer tout ce qui pourrait échouer). Cela attrape la plupart des choses et je renvoie un code d'erreur pour cela.

  2. Renvoyer les codes d'erreur pendant le traitement du travail (et effectuer une restauration si nécessaire).

  3. Exceptions, mais celles-ci ne sont utilisées que pour des choses inattendues.

paxdiablo
la source
1
Ne serait-il pas un peu plus correct d'utiliser des assertions pour les contrats? Si le contrat est rompu, rien ne peut vous sauver.
Paweł Hajdan
@ PawełHajdan, les assertions sont désactivées par défaut, je crois. Cela a le même problème que celui de C asserten ce sens qu'il ne détectera pas de problèmes dans le code de production à moins que vous ne couriez avec des assertions tout le temps. J'ai tendance à voir les assertions comme un moyen de détecter les problèmes pendant le développement, mais uniquement pour les éléments qui seront affirmés ou non de manière cohérente (comme les éléments avec des constantes, pas les éléments avec des variables ou tout autre élément qui peut changer au moment de l'exécution).
paxdiablo
Et douze ans pour répondre à votre question. Je devrais lancer un service d'assistance :-)
paxdiablo
6

Les codes de retour échouent presque à chaque fois au test « Pit of Success ».

  • Il est beaucoup trop facile d'oublier de vérifier un code de retour et d'avoir une erreur de hareng rouge plus tard.
  • Les codes de retour ne contiennent aucune des informations de débogage telles que la pile d'appels, les exceptions internes.
  • Les codes de retour ne se propagent pas, ce qui, avec le point ci-dessus, a tendance à entraîner une journalisation de diagnostic excessive et entrelacée au lieu de la journalisation dans un seul endroit centralisé (gestionnaires d'exceptions au niveau de l'application et du thread).
  • Les codes de retour ont tendance à générer du code désordonné sous la forme de blocs imbriqués `` si ''
  • Le temps consacré par les développeurs à déboguer un problème inconnu qui aurait autrement été une exception évidente (gouffre de succès) est coûteux.
  • Si l'équipe derrière C # n'avait pas l'intention que les exceptions régissent le flux de contrôle, les exceptions ne seraient pas tapées, il n'y aurait pas de filtre «quand» sur les instructions catch, et il n'y aurait pas besoin de l'instruction 'throw' sans paramètre .

Concernant les performances:

  • Les exceptions peuvent être coûteuses en calcul RELATIVES à ne pas lancer du tout, mais elles sont appelées EXCEPTIONS pour une raison. Les comparaisons de vitesse parviennent toujours à supposer un taux d'exception de 100%, ce qui ne devrait jamais être le cas. Même si une exception est 100 fois plus lente, à quel point cela importe-t-il vraiment si cela ne se produit que 1% du temps?
  • À moins que nous ne parlions d'arithmétique à virgule flottante pour les applications graphiques ou quelque chose de similaire, les cycles de processeur sont bon marché par rapport au temps du développeur.
  • Le coût d'un point de vue temporel porte le même argument. Par rapport aux requêtes de base de données, aux appels de service Web ou aux chargements de fichiers, le temps normal de l'application éclipsera le temps d'exception. Les exceptions étaient presque sous la MICROseconde en 2006
    • Je défie quiconque travaille dans .net, de configurer votre débogueur pour qu'il se brise sur toutes les exceptions et ne désactive que mon code et voir combien d'exceptions se produisent déjà que vous ne connaissez même pas.
b_levitt
la source
4

Un excellent conseil que j'ai reçu de The Pragmatic Programmer était quelque chose du genre "votre programme devrait être capable d'exécuter toutes ses fonctionnalités principales sans utiliser d'exceptions du tout".

Smashery
la source
4
Vous l'interprétez mal. Ce qu'ils voulaient dire était "si votre programme lève des exceptions dans ses flux normaux, c'est faux". En d'autres termes, "n'utiliser des exceptions que pour des choses exceptionnelles".
Paweł Hajdan
4

J'ai écrit un article de blog à ce sujet il y a quelque temps.

La surcharge de performance liée à la levée d'une exception ne devrait jouer aucun rôle dans votre décision. Si vous le faites correctement, après tout, une exception est exceptionnelle .

Thomas
la source
Le billet de blog lié concerne davantage l'apparence avant de sauter (vérifications) par rapport à plus facile de demander pardon que la permission (exceptions ou codes de retour). J'y ai répondu avec mes réflexions sur ce problème (indice: TOCTTOU). Mais cette question concerne un autre problème, à savoir dans quelles conditions utiliser le mécanisme d'exception d'un langage plutôt que de renvoyer une valeur avec des propriétés spéciales.
Damian Yerrick
Je suis complètement d'accord. Il semble que j'ai appris une chose ou deux au cours des neuf dernières années;)
Thomas
4

Je n'aime pas les codes de retour car ils provoquent une prolifération du modèle suivant dans votre code

CRetType obReturn = CODE_SUCCESS;
obReturn = CallMyFunctionWhichReturnsCodes();
if (obReturn == CODE_BLOW_UP)
{
  // bail out
  goto FunctionExit;
}

Bientôt, un appel de méthode composé de 4 appels de fonction gonfle avec 12 lignes de gestion des erreurs. Certaines d'entre elles ne se produiront jamais. Si et les cas de changement abondent.

Les exceptions sont plus propres si vous les utilisez bien ... pour signaler des événements exceptionnels ... après quoi le chemin d'exécution ne peut plus continuer. Ils sont souvent plus descriptifs et informatifs que les codes d'erreur.

Si vous avez plusieurs états après un appel de méthode qui doivent être traités différemment (et ne sont pas des cas exceptionnels), utilisez des codes d'erreur ou des paramètres de sortie. Bien que personnellement, j'aie trouvé cela rare.

J'ai un peu cherché le contre-argument de la «pénalité de performance» ... plus dans le monde C ++ / COM mais dans les nouveaux langages, je pense que la différence n'est pas si grande. Dans tous les cas, quand quelque chose explose, les problèmes de performances sont relégués au second plan :)

Gishu
la source
3

Avec n'importe quel compilateur ou environnement d'exécution décent, les exceptions n'entraînent pas de pénalité significative. C'est plus ou moins comme une instruction GOTO qui saute au gestionnaire d'exceptions. De plus, le fait d'avoir des exceptions capturées par un environnement d'exécution (comme la JVM) permet d'isoler et de corriger un bogue beaucoup plus facilement. Je prendrai une NullPointerException en Java sur un segfault en C n'importe quel jour.

Kyle Cronin
la source
2
Les exceptions sont extrêmement coûteuses. Ils doivent parcourir la pile pour trouver des gestionnaires d'exceptions potentiels. Cette promenade de pile n'est pas bon marché. Si une trace de pile est construite, c'est encore plus cher, car alors la pile entière doit être analysée.
Derek Park le
Je suis surpris que les compilateurs ne puissent pas déterminer où l'exception sera interceptée au moins une partie du temps. De plus, le fait qu'une exception modifie le flux de code facilite l'identification exacte de l'endroit où une erreur se produit, à mon avis, compense une pénalité de performance.
Kyle Cronin le
Les piles d'appels peuvent devenir extrêmement complexes au moment de l'exécution, et les compilateurs ne font généralement pas ce genre d'analyse. Même s'ils le faisaient, vous devrez toujours parcourir la pile pour obtenir une trace. Vous devrez également dérouler la pile pour gérer les finallyblocs et les destructeurs pour les objets alloués à la pile.
Derek Park le
Je conviens cependant que les avantages de débogage des exceptions compensent souvent les coûts de performance.
Derek Park
1
Derak Park, les exceptions sont chères lorsqu'elles se produisent. C'est la raison pour laquelle ils ne devraient pas être surutilisés. Mais quand ils ne se produisent pas, ils ne coûtent pratiquement rien.
paercebal
3

J'ai un ensemble de règles simples:

1) Utilisez des codes de retour pour les choses auxquelles vous attendez de votre appelant immédiat qu'il réagisse.

2) Utilisez des exceptions pour les erreurs dont la portée est plus large et dont on peut raisonnablement s'attendre à ce qu'elles soient gérées par quelque chose de plusieurs niveaux au-dessus de l'appelant afin que la conscience de l'erreur n'ait pas à se répandre à travers de nombreuses couches, rendant le code plus complexe.

En Java, je n'ai jamais utilisé que des exceptions non vérifiées, les exceptions vérifiées finissent par n'être qu'une autre forme de code de retour et d'après mon expérience, la dualité de ce qui pourrait être «retourné» par un appel de méthode était généralement plus une entrave qu'une aide.

Kendall Helmstetter Gelner
la source
3

J'utilise des exceptions en python dans des circonstances exceptionnelles et non exceptionnelles.

Il est souvent agréable de pouvoir utiliser une exception pour indiquer que "la demande n'a pas pu être effectuée", par opposition à renvoyer une valeur d'erreur. Cela signifie que vous savez / toujours / que la valeur de retour est le bon type, au lieu de None ou NotFoundSingleton arbitrairement ou quelque chose. Voici un bon exemple où je préfère utiliser un gestionnaire d'exceptions au lieu d'un conditionnel sur la valeur de retour.

try:
    dataobj = datastore.fetch(obj_id)
except LookupError:
    # could not find object, create it.
    dataobj = datastore.create(....)

L'effet secondaire est que lorsqu'un datastore.fetch (obj_id) est exécuté, vous n'avez jamais à vérifier si sa valeur de retour est None, vous obtenez cette erreur immédiatement et gratuitement. Cela va à l'encontre de l'argument selon lequel "votre programme devrait être capable d'exécuter toutes ses fonctionnalités principales sans utiliser d'exceptions du tout".

Voici un autre exemple où les exceptions sont «exceptionnellement» utiles, afin d'écrire du code pour traiter le système de fichiers qui n'est pas soumis à des conditions de concurrence.

# wrong way:
if os.path.exists(directory_to_remove):
    # race condition is here.
    os.path.rmdir(directory_to_remove)

# right way:
try: 
    os.path.rmdir(directory_to_remove)
except OSError:
    # directory didn't exist, good.
    pass

Un appel système au lieu de deux, aucune condition de concurrence. C'est un mauvais exemple car évidemment cela échouera avec une OSError dans plus de circonstances que le répertoire n'existe, mais c'est une solution «assez bonne» pour de nombreuses situations étroitement contrôlées.

Jerub
la source
Le deuxième exemple est trompeur. La mauvaise manière est supposée être incorrecte car le code os.path.rmdir est conçu pour lever une exception. L'implémentation correcte dans le flux de code de retour serait 'if rmdir (...) == FAILED: pass'
MaR
3

Je pense que les codes de retour ajoutent au bruit du code. Par exemple, j'ai toujours détesté l'apparence du code COM / ATL en raison des codes de retour. Il devait y avoir une vérification HRESULT pour chaque ligne de code. Je considère que le code de retour d'erreur est l'une des mauvaises décisions prises par les architectes de COM. Cela rend difficile le regroupement logique du code, donc la révision du code devient difficile.

Je ne suis pas sûr de la comparaison des performances lorsqu'il y a une vérification explicite du code de retour à chaque ligne.

rpattabi
la source
1
COM wad conçu pour être utilisable par des langages qui ne prennent pas en charge les exceptions.
Kevin le
C'est un bon point. Il est logique de traiter les codes d'erreur pour les langages de script. Au moins VB6 masque bien les détails du code d'erreur en les encapsulant dans l'objet Err, ce qui aide quelque peu à un code plus propre.
rpattabi
Je ne suis pas d'accord: VB6 n'enregistre que la dernière erreur. Combiné avec le tristement célèbre "sur erreur reprendre ensuite", vous manquerez complètement la source de votre problème, au moment où vous le verrez. Notez que c'est la base de la gestion des erreurs dans Win32 APII (voir la fonction GetLastError)
paercebal
2

Je préfère utiliser des exceptions pour la gestion des erreurs et renvoyer des valeurs (ou des paramètres) comme résultat normal d'une fonction. Cela donne un schéma de gestion des erreurs simple et cohérent et, si cela est fait correctement, cela donne un code beaucoup plus propre.

Trent
la source
2

L'une des grandes différences est que les exceptions vous obligent à gérer une erreur, alors que les codes de retour d'erreur peuvent être décochés.

Les codes de retour d'erreur, s'ils sont utilisés de manière intensive, peuvent également provoquer un code très laid avec de nombreux tests if similaires à ce formulaire:

if(function(call) != ERROR_CODE) {
    do_right_thing();
}
else {
    handle_error();
}

Personnellement, je préfère utiliser des exceptions pour les erreurs qui DEVRAIENT ou DOIVENT être traitées par le code appelant, et n'utiliser que des codes d'erreur pour les "échecs attendus" où le retour de quelque chose est réellement valide et possible.

Daniel Bruce
la source
Au moins en C / C ++ et gcc, vous pouvez donner à une fonction un attribut qui générera un avertissement lorsque sa valeur de retour est ignorée.
Paweł Hajdan
phjr: Bien que je ne sois pas d'accord avec le modèle "code d'erreur de retour", votre commentaire devrait peut-être devenir une réponse complète. Je trouve cela assez intéressant. À tout le moins, cela m'a donné une information utile.
paercebal
2

Il y a de nombreuses raisons de préférer les exceptions au code retour:

  • Habituellement, pour la lisibilité, les gens essaient de minimiser le nombre d'instructions de retour dans une méthode. Ce faisant, les exceptions empêchent d'effectuer un travail supplémentaire dans un état incoorect, et empêchent ainsi d'endommager potentiellement plus de données.
  • Les exceptions sont généralement plus verbeuses et plus facilement extensibles que la valeur de retour. Supposons qu'une méthode renvoie un nombre naturel et que vous utilisez des nombres négatifs comme code de retour lorsqu'une erreur se produit, si la portée de votre méthode change et renvoie maintenant des entiers, vous devrez modifier tous les appels de méthode au lieu de simplement peaufiner un peu l'éxéption.
  • Les exceptions permettent de séparer plus facilement la gestion des erreurs du comportement normal. Ils permettent de s'assurer que certaines opérations fonctionnent en quelque sorte comme une opération atomique.
gadget
la source
2

Les exceptions ne concernent pas la gestion des erreurs, OMI. Les exceptions ne sont que cela; événements exceptionnels auxquels vous ne vous attendiez pas. Utilisez avec prudence je dis.

Les codes d'erreur peuvent être OK, mais renvoyer 404 ou 200 à partir d'une méthode est mauvais, IMO. Utilisez plutôt des enums (.Net), ce qui rend le code plus lisible et plus facile à utiliser pour les autres développeurs. De plus, vous n'avez pas à maintenir un tableau sur les nombres et les descriptions.

Aussi; le modèle try-catch-finally est un anti-pattern dans mon livre. Try-finally peut être bon, try-catch peut aussi être bon, mais try-catch-finally n'est jamais bon. try-finally peut souvent être remplacé par une instruction "using" (modèle IDispose), ce qui est mieux IMO. Et Try-catch où vous attrapez réellement une exception que vous êtes capable de gérer est bon, ou si vous faites ceci:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Donc, tant que vous laissez l'exception continuer à bouillonner, c'est OK. Un autre exemple est celui-ci:

try{
    dbHasBeenUpdated = db.UpdateAll(somevalue); // true/false
}
catch (ConnectionException ex) {
    logger.Exception(ex, "Connection failed");
    dbHasBeenUpdated = false;
}

Ici, je gère réellement l'exception; ce que je fais en dehors du try-catch lorsque la méthode de mise à jour échoue est une autre histoire, mais je pense que mon point a été fait. :)

Pourquoi alors try-catch-finally est un anti-pattern? Voici pourquoi:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}
finally {
    db.Close();
}

Que se passe-t-il si l'objet db a déjà été fermé? Une nouvelle exception est lancée et doit être gérée! C'est mieux:

try{
    using(IDatabase db = DatabaseFactory.CreateDatabase()) {
        db.UpdateAll(somevalue);
    }
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Ou, si l'objet db n'implémente pas IDisposable, procédez comme suit:

try{
    try {
        IDatabase db = DatabaseFactory.CreateDatabase();
        db.UpdateAll(somevalue);
    }
    finally{
        db.Close();
    }
}
catch (DatabaseAlreadyClosedException dbClosedEx) {
    logger.Exception(dbClosedEx, "Database connection was closed already.");
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

C'est mes 2 cents en tout cas! :)

noocyte
la source
Ce sera étrange si un objet a .Close () mais n'a pas .Dispose ()
abatishchev
J'utilise uniquement Close () comme exemple. N'hésitez pas à y penser comme autre chose. Comme je le dis; le modèle d'utilisation doit être utilisé (doh!) s'il est disponible. Cela implique bien sûr que la classe implémente IDisposable et que vous pouvez donc appeler Dispose.
noocyte
1

Je n'utilise que des exceptions, pas de codes de retour. Je parle de Java ici.

La règle générale que je suis est que si j'ai une méthode appelée, doFoo()il s'ensuit que si elle ne "fait pas foo", pour ainsi dire, quelque chose d'exceptionnel s'est produit et une exception devrait être lancée.

SCdF
la source
1

Une chose que je crains à propos des exceptions est que lancer une exception va bousiller le flux de code. Par exemple si vous faites

void foo()
{
  MyPointer* p = NULL;
  try{
    p = new PointedStuff();
    //I'm a module user and  I'm doing stuff that might throw or not

  }
  catch(...)
  {
    //should I delete the pointer?
  }
}

Ou pire encore, que se passerait-il si j'avais supprimé quelque chose que je n'aurais pas dû, mais que j'étais jeté à attraper avant de faire le reste du nettoyage. Lancer a mis beaucoup de poids sur le pauvre utilisateur IMHO.

Robert Gould
la source
C'est à cela que sert l'instruction <code> finally </code>. Mais, hélas, ce n'est pas dans le standard C ++ ...
Thomas
En C ++, vous devez vous en tenir à la règle de base "Acquérir des ressources dans construcotor et les libérer dans destrucotor. Pour ce cas particulier, auto_ptr fera parfaitement l'affaire.
Serge
Thomas, tu as tort. C ++ ne l'a finalement pas car il n'en a pas besoin. Il a RAII à la place. La solution de Serge est une solution utilisant RAII.
paercebal
Robert, utilisez la solution de Serge et vous verrez que votre problème disparaîtra. Maintenant, si vous écrivez plus d'essais / captures que de lancers, alors (en jugeant votre commentaire) peut-être que vous avez un problème dans votre code. Bien sûr, utiliser catch (...) sans relancer est généralement mauvais, car il cache l'erreur pour mieux l'ignorer.
paercebal
1

Ma règle générale dans l'argument exception vs code de retour:

  • Utilisez des codes d'erreur lorsque vous avez besoin d'une localisation / internationalisation - dans .NET, vous pouvez utiliser ces codes d'erreur pour référencer un fichier de ressources qui affichera ensuite l'erreur dans la langue appropriée. Sinon, utilisez des exceptions
  • N'utilisez les exceptions que pour les erreurs vraiment exceptionnelles. Si c'est quelque chose qui arrive assez souvent, utilisez un code d'erreur booléen ou enum.
Jon Limjap
la source
Il n'y a aucune raison que vous ne puissiez pas utiliser d'exception lorsque vous utilisez l10n / i18n. Les exceptions peuvent également contenir des informations localisées.
gizmo
1

Je ne trouve pas que les codes de retour soient moins laids que les exceptions. À l'exception, vous avez le try{} catch() {} finally {}où comme avec les codes de retour que vous avez if(){}. J'avais l'habitude de craindre des exceptions pour les raisons données dans le message; vous ne savez pas si le pointeur doit être effacé, qu'est-ce que vous avez. Mais je pense que vous avez les mêmes problèmes en ce qui concerne les codes de retour. Vous ne connaissez pas l'état des paramètres à moins de connaître certains détails sur la fonction / méthode en question.

Quoi qu'il en soit, vous devez gérer l'erreur si possible. Vous pouvez tout aussi facilement laisser une exception se propager au niveau supérieur que d'ignorer un code de retour et laisser le programme segfault.

J'aime l'idée de renvoyer une valeur (énumération?) Pour les résultats et une exception pour un cas exceptionnel.

Jonathan Adelson
la source
0

Pour un langage comme Java, j'irais avec Exception car le compilateur donne une erreur de compilation si les exceptions ne sont pas gérées, ce qui oblige la fonction appelante à gérer / lever les exceptions.

Pour Python, je suis plus en conflit. Il n'y a pas de compilateur, il est donc possible que l'appelant ne gère pas l'exception levée par la fonction menant aux exceptions d'exécution. Si vous utilisez des codes de retour, vous pouvez avoir un comportement inattendu s'ils ne sont pas gérés correctement et si vous utilisez des exceptions, vous pouvez obtenir des exceptions d'exécution.

2ank3e
la source
0

Je préfère généralement les codes de retour car ils laissent l'appelant décider si l'échec est exceptionnel .

Cette approche est typique du langage Elixir.

# I care whether this succeeds. If it doesn't return :ok, raise an exception.
:ok = File.write(path, content)

# I don't care whether this succeeds. Don't check the return value.
File.write(path, content)

# This had better not succeed - the path should be read-only to me.
# If I get anything other than this error, raise an exception.
{:error, :erofs} = File.write(path, content)

# I want this to succeed but I can handle its failure
case File.write(path, content) do
  :ok => handle_success()
  error => handle_error(error)
end

Les gens ont mentionné que les codes de retour peuvent vous amener à avoir beaucoup d' ifinstructions imbriquées , mais que cela peut être géré avec une meilleure syntaxe. Dans Elixir, l' withinstruction nous permet de séparer facilement une série de valeurs de retour happy-path de tout échec.

with {:ok, content} <- get_content(),
  :ok <- File.write(path, content) do
    IO.puts "everything worked, happy path code goes here"
else
  # Here we can use a single catch-all failure clause
  # or match every kind of failure individually
  # or match subsets of them however we like
  _some_error => IO.puts "one of those steps failed"
  _other_error => IO.puts "one of those steps failed"
end

Elixir a toujours des fonctions qui soulèvent des exceptions. Pour revenir à mon premier exemple, je pourrais faire l'un ou l'autre de ces éléments pour lever une exception si le fichier ne peut pas être écrit.

# Raises a generic MatchError because the return value isn't :ok
:ok = File.write(path, content)

# Raises a File.Error with a descriptive error message - eg, saying
# that the file is read-only
File.write!(path, content)

Si, en tant qu'appelant, je sais que je veux déclencher une erreur si l'écriture échoue, je peux choisir d'appeler File.write!au lieu de File.write. Ou je peux choisir d'appeler File.writeet de gérer différemment chacune des raisons possibles de l'échec.

Bien sûr, il est toujours possible de faire rescueune exception si nous le voulons. Mais comparé à la gestion d'une valeur de retour informative, cela me semble gênant. Si je sais qu'un appel de fonction peut échouer ou même doit échouer, son échec n'est pas un cas exceptionnel.

Nathan Long
la source