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 pourquoi vous préférez l'une à l'autre.
Je pose cette question 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'il ne le souhaite 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.
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 un très bon raison de ne pas avoir créé votre objet sur la pile).
Ils sont verbeux et ont tendance à évoluer vers 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 séparer proprement et traitement des erreurs, qui peut être une chose bonne.
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, supposez que quelqu'un oublie de gérer son éventuelle erreur (cela se produit ...). L'erreur est ignorée lorsqu'elle est "renvoyée" et peut éventuellement 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'il n'explose pas, cependant ... Vous devez donc choisir avec soin.
Disons que nous avons les fonctions suivantes:
Quel est le type de retour de doTryToDoSomethingWithAllThisMess si l'une de ses fonctions appelées échoue?
Les opérateurs ne peuvent pas renvoyer de code d'erreur. Les constructeurs C++ ne le peuvent pas non plus.
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.
Vous pouvez envoyer différentes classes pour chaque type d'exception. Les exceptions de ressources (c'est-à-dire par manque de mémoire) devraient être légères, mais tout le reste pourrait être aussi lourd que nécessaire (j'aime l'exception Java me donnant toute la pile).
Chaque prise peut alors être spécialisée.
En règle générale, vous ne devez pas masquer une erreur. Si vous ne relancez pas, au moins, enregistrez l'erreur dans un fichier, ouvrez une boîte de message, peu importe ...
Le problème à l'exception est que leur utilisation excessive produira du code plein d'essais/captures. Mais le problème est ailleurs: qui essaie/attrape 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.
Assurez-vous de les attraper avant qu'ils ne mettent votre fil à genoux ou se propagent dans votre boucle de messages Windows.
Donc je suppose que la solution est de jeter quand quelque chose devrait pas se produire. Et quand quelque chose peut se produire, utilisez un code retour ou un paramètre pour permettre à l'utilisateur d'y réagir.
Donc, la seule question est "qu'est-ce 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 ...)
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". Il dépendra, selon les valeurs d'un fichier de configuration, quelles que soient les options de compilation de débogage/libération:
En développement et en test, cela permet à l'utilisateur de localiser le problème exactement quand il est détecté, et non après (lorsqu'un code se soucie 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 lors du 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.
Lorsque vous codez à l'aide de codes retour, vous vous préparez à l'échec et vous espérez que votre forteresse de tests sera suffisamment sécurisée.
Lorsque vous codez à l'aide d'une exception, vous savez que votre code peut échouer et placez généralement la prise de contre-feu à la position stratégique choisie dans votre code. Mais en général, votre code concerne davantage "ce qu'il doit faire" que "ce que je crains qu'il se produise".
Mais lorsque vous codez, vous devez utiliser le meilleur outil à votre disposition, et parfois, c'est "Ne jamais cacher une erreur et l'afficher le plus tôt possible". La macro dont j'ai parlé ci-dessus suit cette philosophie.
J'utilise les deux en fait.
J'utilise des codes retour s'il s'agit d'une erreur connue et possible. Si c'est un scénario qui, je le sais, peut arriver 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.
Selon le chapitre 7 intitulé "Exceptions" dans Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries, de nombreuses raisons sont données pour expliquer pourquoi l'utilisation d'exceptions sur les valeurs de retour est nécessaire pour OO frameworks tels que C #.
C'est peut-être la raison la plus convaincante (page 179):
"Les exceptions s'intègrent bien aux 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 les fonctions dans les langages non OO. Par exemple, dans le cas des constructeurs, les surcharges d'opérateurs et 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 standardiser le rapport d'erreur basé sur la valeur de retour pour les frameworks orientés objet. ne méthode de rapport d'erreur, telle que exceptions, qui est hors bande de la signature de méthode est la seule option. "
Ma préférence (en C++ et Python) est d'utiliser des exceptions. Les installations fournies par la langue en font un processus bien défini pour à la fois lever, intercepter et (si nécessaire) relancer les exceptions, ce qui rend le modèle facile à voir et à utiliser. Conceptuellement, c'est plus propre que les codes de retour, dans la mesure où des exceptions spécifiques peuvent être définies par leurs noms et accompagnées d'informations supplémentaires. Avec un code retour, vous êtes limité à la seule valeur d'erreur (sauf si vous souhaitez définir un objet ReturnStatus ou quelque chose).
À moins que le code que vous écrivez soit critique dans le temps, la surcharge associée au déroulement de la pile n'est pas suffisamment importante pour vous inquiéter.
Les exceptions ne doivent être renvoyées que lorsque quelque chose se produit auquel vous ne vous attendiez pas.
L'autre point d'exception, historiquement, est que les codes de retour sont intrinsèquement propriétaires, parfois un 0 peut être renvoyé 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écisent bien: "Quelque chose ne va pas, 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 "heres n résultats de la fonction, et il a simplement fonctionné de cette façon"
En Java, j'utilise (dans l'ordre suivant):
Conception par contrat (s'assurer que les conditions préalables sont remplies avant d'essayer quoi que ce soit qui pourrait échouer). Cela attrape la plupart des choses et je renvoie un code d'erreur pour cela.
Renvoyer des codes d'erreur lors du traitement du travail (et effectuer une restauration si nécessaire).
Des exceptions, mais celles-ci sont utilisées niquement pour des choses inattendues.
Un excellent conseil que j'ai reçu de The Pragmatic Programmer était quelque chose dans le sens de "votre programme devrait être capable d'exécuter toutes ses fonctionnalités principales sans utiliser d'exceptions du tout".
J'ai écrit un blog post à ce sujet il y a quelque temps.
Les frais généraux liés au lancement d'une exception ne devraient pas jouer de rôle dans votre décision. Si vous le faites bien, après tout, une exception est exceptionnelle .
Les codes de retour échouent presque à chaque fois au test " Pit of Success ".
Concernant les performances:
Je n'aime pas les codes de retour, car ils provoquent la prolifération du modèle suivant dans tout 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 s'enrichit de 12 lignes de gestion des erreurs. Certaines ne se produiront jamais. Les cas If et Switch 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 pas 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'ai trouvé que c'était rare ..
J'ai un peu chassé 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 :)
Je crois 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 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, ainsi 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 retour à chaque ligne.
Avec tout compilateur décent ou environnement d'exécution, les exceptions n'entraînent pas de pénalité importante. Cela ressemble plus ou moins à une instruction GOTO qui passe 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 dans Java sur un segfault en C n'importe quel jour.
J'utilise des exceptions dans python dans des circonstances à la fois 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 au renvoi d'une valeur d'erreur. Cela signifie que vous/toujours/savez que la valeur de retour est le bon type, au lieu de arbitrairement None ou NotFoundSingleton ou quelque chose. Voici un bon exemple où je préfère utiliser un gestionnaire d'exceptions au lieu d'une conditionnelle à 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 immédiatement cette erreur gratuitement. Ceci est contraire à l'argument, "votre programme devrait être capable d'exécuter toutes ses fonctionnalités principales sans utiliser d'exceptions du tout".
Voici un autre exemple de cas où les exceptions sont "exceptionnellement" utiles, afin d'écrire du code pour gérer le système de fichiers qui n'est pas soumis aux 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 cela échouera évidemment avec une OSError dans plus de circonstances que le répertoire n'existe pas, mais c'est une solution "assez bonne" pour de nombreuses situations étroitement contrôlées.
Je préfère utiliser des exceptions pour la gestion des erreurs et les valeurs de retour (ou paramètres) comme résultat normal d'une fonction. Cela donne un schéma de gestion des erreurs facile et cohérent et s'il est fait correctement, il rend le code beaucoup plus propre.
L'une des grandes différences est que les exceptions vous obligent à gérer une erreur, tandis que les codes retour d'erreur peuvent être décochés.
Les codes de retour d'erreur, s'ils sont largement utilisés, peuvent également provoquer un code très laid avec beaucoup de 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 des codes d'erreur que pour les "défaillances attendues" où le retour de quelque chose est réellement valide et possible.
Il existe de nombreuses raisons de préférer les exceptions au code retour:
Les exceptions ne concernent pas la gestion des erreurs, OMI. Les exceptions ne sont que cela; des événements exceptionnels auxquels vous ne vous attendiez pas. À utiliser avec prudence, dis-je.
Les codes d'erreur peuvent être OK, mais renvoyer 404 ou 200 à partir d'une méthode est mauvais, IMO. Utilisez plutôt des énumérations (.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 motif try-catch-finally est un anti-motif dans mon livre. Essayer-finalement peut être bon, essayer-attraper peut aussi être bon mais essayer-attraper-finalement 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 le suivant:
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 argument a été avancé. :)
Pourquoi est-ce que try-catch-finalement est un anti-modèle? 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 levé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 quand même! :)
J'utilise uniquement des exceptions, pas de codes retour. Je parle de Java ici.
La règle générale que je respecte est que si j'ai une méthode appelée doFoo()
, il s'ensuit que si elle ne fait pas "foo", pour ainsi dire, alors quelque chose d'exceptionnel s'est produit et une exception doit être levée.
Une chose que je crains à propos des exceptions, c'est que lever une exception va gâcher le flux de code. Par exemple, si vous le 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, si je supprimais quelque chose que je n'aurais pas dû, mais que je me faisais attraper avant de faire le reste du nettoyage. Lancer a mis beaucoup de poids sur le pauvre utilisateur à mon humble avis.
Ma règle générale dans l'argument exception/code retour:
J'ai un ensemble simple de règles:
1) Utilisez les codes de retour pour les choses auxquelles vous vous attendez à ce que votre correspondant immédiat 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 plusieurs niveaux au-dessus de l'appelant afin que la connaissance de l'erreur n'ait pas à se propager à travers plusieurs couches, ce qui rend le code plus complexe.
Dans Java je n'ai utilisé que des exceptions non contrôlées, les exceptions vérifiées finissent par n'être qu'une autre forme de code retour et d'après mon expérience, la dualité de ce qui pourrait être "renvoyé" par un appel de méthode était généralement plus une gêne plutôt qu'une aide.
Je ne trouve pas les codes de retour moins moches que les exceptions. À l'exception, vous avez la try{} catch() {} finally {}
où, comme pour les codes retour, vous avez if(){}
. J'avais l'habitude de craindre les exceptions pour les raisons données dans le post; vous ne savez pas si le pointeur doit être effacé, qu'avez-vous. 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, sauf si vous connaissez 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 qu'ignorer un code 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.
Pour un langage comme Java, j'irais avec Exception car le compilateur donne une erreur de temps de compilation si les exceptions ne sont pas gérées, ce qui force 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 à des exceptions d'exécution. Si vous utilisez des codes retour, vous pourriez avoir un comportement inattendu s'il n'est pas géré correctement et si vous utilisez des exceptions, vous pouvez obtenir des exceptions d'exécution.