Pourquoi utiliser essayer… enfin sans clause de renvoi?


79

La façon classique de programmer est avec try ... catch. Quand est-il approprié d'utiliser trysans catch?

En Python, ce qui suit semble légal et peut avoir un sens:

try:
  #do work
finally:
  #do something unconditional

Cependant, le code n'a catchrien. De même, on pourrait penser en Java que:

try {
    //for example try to get a database connection
}
finally {
  //closeConnection(connection)
}

Ça a l'air bien et du coup je n'ai plus à m'inquiéter des types d'exceptions, etc. Si c'est une bonne pratique, quand l'est-il? Sinon, quelles sont les raisons pour lesquelles ce n'est pas une bonne pratique ou n'est pas légal? (Je n'ai pas compilé le source. Je pose la question car cela pourrait être une erreur de syntaxe pour Java. J'ai vérifié que le Python compile sûrement.)

Un problème connexe que j'ai rencontré est le suivant: je continue à écrire la fonction / méthode, à la fin de laquelle il doit renvoyer quelque chose. Cependant, il se peut que ce soit dans un endroit qui ne devrait pas être atteint et qui doit être un point de retour. Ainsi, même si je gère les exceptions ci-dessus, je renvoie toujours NULLune chaîne vide dans le code qui ne doit pas être atteint, souvent la fin de la méthode / fonction. J'ai toujours réussi à restructurer le code afin qu'il n'en soit pas obligé return NULL, car cela ne semble absolument pas être une bonne pratique.


4
En Java, pourquoi ne pas mettre l'instruction return à la fin du bloc try?
kevin cline

2
Je suis triste que try..finally et try..catch utilisent tous les deux le mot-clé try, mis à part que tous les deux commençant par try, ils ont 2 constructions totalement différentes.
Pieter B

3
try/catchn'est pas "la manière classique de programmer." C'est la manière classique de programmer en C ++ , car le C ++ ne dispose pas d'une construction try / finally appropriée, ce qui signifie que vous devez implémenter des changements d'état réversibles garantis à l'aide de piratages laids impliquant RAII. Mais les langues OO décentes ne rencontrent pas ce problème, car elles fournissent try / finally. Il est utilisé dans un but très différent de try / catch.
Mason Wheeler

3
Je le vois souvent avec les ressources de connexion externes. Vous voulez l'exception mais vous devez vous assurer de ne pas laisser une connexion ouverte, etc. Si vous l'attrapiez, vous la renverriez au niveau suivant dans tous les cas, dans certains cas.
Rig

4
@MasonWheeler "Laids", s'il vous plaît, expliquez ce qui ne va pas avec le fait qu'un objet gère son propre nettoyage?
Baldrickk

Réponses:


146

Cela dépend si vous pouvez traiter des exceptions qui peuvent être soulevées à ce stade ou non.

Si vous pouvez gérer les exceptions localement, vous devriez le faire, et il est préférable de gérer l'erreur aussi près que possible de l'endroit où elle est levée.

Si vous ne pouvez pas les gérer localement, le simple fait de try / finallybloquer est tout à fait raisonnable - en supposant qu'il y ait du code que vous devez exécuter, que la méthode ait réussi ou non. Par exemple (du commentaire de Neil ), ouvrir un flux, puis passer ce flux à une méthode interne à charger est un excellent exemple du moment où vous auriez besoin try { } finally { }, en utilisant la clause finally pour vous assurer que le flux est fermé indépendamment du succès ou de la perte. échec de la lecture.

Cependant, vous aurez toujours besoin d'un gestionnaire d'exceptions quelque part dans votre code - à moins que vous ne souhaitiez que votre application plante complètement, bien sûr. Cela dépend de l'architecture de votre application et de l'emplacement exact de ce gestionnaire.


8
"et il est préférable de gérer l'erreur aussi près que possible de l'endroit où elle est soulevée." hein, ça dépend. Si vous pouvez récupérer et terminer la mission (pour ainsi dire), oui bien sûr. Si vous ne pouvez pas, il vaut mieux laisser l'exception aller jusqu'au sommet, où une intervention (probable) de l'utilisateur sera nécessaire pour faire face à ce qui s'est passé.
Will

13
@will - c'est pourquoi j'ai utilisé l'expression "autant que possible".
ChrisF

8
Bonne réponse, mais j'ajouterais un exemple: Ouvrir un flux et transmettre ce flux à une méthode interne à charger est un excellent exemple du moment où vous en auriez besoin try { } finally { }. Profitez de la clause finally pour vous assurer que le flux est finalement fermé, quel que soit le succès / échec.
Neil

parce que parfois tout en haut est aussi proche que possible
Newtopian

"juste essayer / bloquer est parfaitement raisonnable" cherchait exactement cette réponse. merci @ChrisF
neal

36

Le finallybloc est utilisé pour le code qui doit toujours être exécuté, qu’une condition d’erreur (exception) se soit produite ou non.

Le code du finallybloc est exécuté après la fin du trybloc et, si une exception interceptée se produit, après la fin du catchbloc correspondant . Il est toujours exécuté, même si une exception non interceptée s'est produite dans le bloc tryou catch.

Le finallybloc est généralement utilisé pour la fermeture de fichiers, de connexions réseau, etc. ouverts dans le trybloc. La raison en est que la connexion de fichier ou de réseau doit être fermée, que l'opération utilisant cette connexion de fichier ou de réseau ait abouti ou non.

Il faut veiller à ce que le finallybloc ne lance pas une exception. Par exemple, assurez-vous de vérifier toutes les variables pour null, etc.


8
+1: C'est idiomatique pour "doit être nettoyé". La plupart des utilisations de try-finallypeuvent être remplacées par une withdéclaration.
S.Lott

12
Divers langages ont des améliorations extrêmement utiles, spécifiques à chaque langage, de la try/finallyconstruction. C # a using, Python a with, etc.
yfeldblum

5
@yfeldblum - il existe une différence subtile entre usinget try-finally, car la Disposeméthode ne sera pas appelée par le usingbloc si une exception se produit dans le IDisposableconstructeur de l' objet. try-finallyvous permet d'exécuter du code même si le constructeur de l'objet lève une exception.
Scott Whitlock

5
@ScottWhitlock: C'est une bonne chose? Qu'est-ce que vous essayez de faire, appeler une méthode sur un objet non construit? C'est un milliard de mauvaises.
DeadMG


17

Un exemple où essayer ... enfin sans clause catch est approprié (et encore plus idiomatique ) en Java est l'utilisation de Lock dans le package de verrous d'utilitaires simultanés.

  • Voici comment cela est expliqué et justifié dans la documentation de l'API (la police en gras dans quote est la mienne):

    ... l'absence de verrouillage structuré en blocs supprime la libération automatique des verrous qui se produisent avec les méthodes et les instructions synchronisées. Dans la plupart des cas, l'idiome suivant devrait être utilisé :

     Lock l = ...;
     l.lock();
     try {
         // access the resource protected by this lock
     } finally {
         l.unlock();
     }
    

    Lorsque le verrouillage et le déverrouillage ont lieu dans différentes portées, vous devez vous assurer que tout le code exécuté tant que le verrou est maintenu est protégé par try-finally ou try-catch pour vous assurer que le verrou est libéré lorsque cela est nécessaire .


Puis-je mettre à l' l.lock()intérieur essayer? try{ l.lock(); }finally{l.unlock();}
RMachnik

1
techniquement, vous pouvez. Je ne l'ai pas mis là car sémantiquement, c'est moins logique. trydans cet extrait est destiné à envelopper l'accès aux ressources, pourquoi le polluer avec quelque chose qui
gnat

4
Vous pouvez le faire, mais en cas d' l.lock()échec, le finallybloc sera toujours exécuté s'il se l.lock()trouve à l'intérieur du trybloc. Si vous le faites comme le suggère gnat, le finallyblocage ne fonctionnera que si nous savons que le verrou a été acquis.
Wtrmute

12

Au niveau de base catchet finallyrésoudre deux problèmes liés mais différents:

  • catch est utilisé pour gérer un problème signalé par le code que vous avez appelé
  • finally est utilisé pour nettoyer les données / ressources créées / modifiées par le code actuel, peu importe si un problème survient ou non

Donc, les deux sont liés d'une manière ou d'une autre à des problèmes (exceptions), mais c'est à peu près tout ce qu'ils ont en commun.

Une différence importante est que le finallybloc doit être dans la même méthode où les ressources ont été créées (pour éviter les fuites de ressources) et ne peut pas être placé à un niveau différent dans la pile d'appels.

Le catchest cependant une autre affaire: le bon endroit pour cela dépend où vous pouvez réellement gérer l'exception. Il est inutile de capturer une exception dans un endroit où vous ne pouvez rien y faire. Il est donc parfois préférable de la laisser simplement tomber.


2
Nitpick: "... le bloc finally doit être dans la même méthode que celle où les ressources ont été créées ..." . C'est certainement une bonne idée de le faire de cette façon, car il est plus facile de voir qu'il n'y a pas de fuite de ressources. Cependant, ce n'est pas une condition préalable nécessaire; c'est-à-dire que vous n'avez pas à le faire de cette façon. Vous pouvez libérer la ressource dans finallyune instruction try englobante (de manière statique ou dynamique) ... tout en restant étanche à 100%.
Stephen C

6

@yfeldblum a la bonne réponse: essayer-enfin sans instruction catch devrait généralement être remplacé par une construction de langage appropriée.

En C ++, il utilise RAII et les constructeurs / destructeurs; en Python c'est une withdéclaration; et en C #, c'est une usingdéclaration.

Celles-ci sont presque toujours plus élégantes parce que les codes d'initialisation et de finalisation se trouvent à un seul endroit (l'objet abstrait) plutôt qu'à deux endroits.


2
Et en Java, il s’agit de blocs ARM
MarkJ

2
Supposons que vous ayez un objet mal conçu (par exemple, un objet qui n'implémente pas correctement IDisposable en C #) et qui n'est pas toujours viable.
Mootinator

1
@mootinator: ne pouvez-vous pas hériter de l'objet mal conçu et le réparer?
Neil G

2
Ou encapsulation? Bah. Je veux dire oui, bien sûr.
Mootinator

4

Dans de nombreuses langues, une finallyinstruction est également exécutée après l'instruction return. Cela signifie que vous pouvez faire quelque chose comme:

try {
  // Do processing
  return result;
} finally {
  // Release resources
}

Qui libère les ressources, quelle que soit la manière dont la méthode a été terminée, avec une exception ou une instruction return classique.

Que cela soit bon ou mauvais fait l’objet d’un débat, mais try {} finally {}ne se limite pas toujours à la gestion des exceptions.


0

Je pourrais invoquer la colère des pythonistes (je ne sais pas car je n’utilise pas beaucoup Python) ou des programmeurs d’autres langages avec cette réponse, mais à mon avis, la plupart des fonctions ne devraient pas être catchbloquées, idéalement. Pour montrer pourquoi, permettez-moi de mettre cela en contraste avec la propagation manuelle du code d’erreur du type que j’avais dû faire lorsque je travaillais avec Turbo C à la fin des années 80 et au début des années 90.

Supposons donc que nous ayons une fonction pour charger une image ou quelque chose comme ça en réponse à un utilisateur sélectionnant un fichier image à charger, et ceci est écrit en C et en assembleur:

entrez la description de l'image ici

J'ai omis certaines fonctions de bas niveau, mais nous pouvons voir que j'ai identifié différentes catégories de fonctions, codées par couleur, en fonction de leurs responsabilités en ce qui concerne le traitement des erreurs.

Point de défaillance et de récupération

Maintenant, il n’a jamais été difficile d’écrire les catégories de fonctions que j’appelle le "point de défaillance possible" (celles qui throw, par exemple) et les fonctions "récupération et rapport d’erreur" (celles qui catch, par exemple).

Il était toujours facile d’écrire correctement avant la gestion des exceptions pour ces fonctions, puisqu’une fonction pouvant rencontrer un échec externe, comme l’absence d’allocation de mémoire, peut simplement renvoyer un NULLou 0ou -1ou définir un code d’erreur global ou quelque chose de similaire . Et la récupération / le signalement des erreurs était toujours facile car une fois que vous avez parcouru la pile d’appels jusqu’à un point où il était logique de récupérer et de signaler les échecs, il vous suffit de prendre le code et / ou le message d’erreur et de le signaler à l’utilisateur. Et naturellement, une fonction à la feuille de cette hiérarchie qui ne peut jamais, jamais échouer, peu importe la façon dont elle a été modifiée à l'avenir ( Convert Pixel) est extrêmement simple à écrire correctement (du moins en ce qui concerne la gestion des erreurs).

Propagation d'erreur

Cependant, les fonctions fastidieuses sujettes aux erreurs humaines étaient les propagateurs d'erreurs , celles qui ne rencontraient pas directement l'échec mais appelaient des fonctions susceptibles d'échouer quelque part plus profondément dans la hiérarchie. À ce moment - là, Allocate Scanlinepourrait avoir à gérer un échec de mallocpuis retourner une erreur à Convert Scanlines, puis Convert Scanlinesdevra vérifier cette erreur et le transmettre à Decompress Image, puis Decompress Image->Parse Image, et Parse Image->Load Image, et Load Imageà la commande utilisateur final où l'erreur est finalement rapporté .

C’est là que beaucoup d’êtres humains commettent des erreurs puisqu’il suffit d’un propagateur d’erreur pour vérifier et transmettre l’erreur pour que toute la hiérarchie des fonctions s’effondre lorsqu’il s’agit de gérer correctement l’erreur.

De plus, si des fonctions retournent des codes d’erreur, nous perdons la capacité, par exemple, de 90% de notre base de code, de renvoyer des valeurs d’intérêt en cas de succès, car de nombreuses fonctions devraient réserver leur valeur de retour pour renvoyer un code d’erreur sur échec .

Réduction des erreurs humaines: codes d'erreur globaux

Alors, comment pouvons-nous réduire le risque d'erreur humaine? Ici, je pourrais même invoquer la colère de certains programmeurs C, mais une amélioration immédiate à mon avis consiste à utiliser des codes d'erreur globaux , comme OpenGL avec glGetError. Cela libère au moins les fonctions pour renvoyer des valeurs d’intérêt significatives en cas de succès. Il existe des moyens de rendre ce thread-safe et efficace lorsque le code d'erreur est localisé dans un thread.

Il existe également des cas où une fonction peut rencontrer une erreur mais il est relativement inoffensif de continuer un peu plus longtemps avant de revenir prématurément à la suite de la découverte d'une erreur précédente. Cela permet qu'une telle chose se produise sans qu'il soit nécessaire de vérifier les erreurs par rapport à 90% des appels de fonction effectués dans chaque fonction. Elle peut donc permettre une gestion correcte des erreurs sans être aussi minutieuse.

Réduire l'erreur humaine: traitement des exceptions

Cependant, la solution ci-dessus nécessite toujours autant de fonctions pour traiter l’aspect flux de contrôle de la propagation manuelle des erreurs, même si elle aurait pu réduire le nombre de lignes if error happened, return errorde code de type manuel . Cela ne l'éliminerait pas complètement car il faudrait toujours au moins un endroit pour rechercher une erreur et revenir pour presque chaque fonction de propagation d'erreur. C'est donc à ce moment-là que la gestion des exceptions entre en scène pour sauver la journée (sorta).

Mais l'intérêt de la gestion des exceptions ici est de libérer le besoin de traiter de l'aspect de flux de contrôle de la propagation d'erreur manuelle. Cela signifie que sa valeur est liée à la possibilité d’éviter d’écrire un grand nombre de catchblocs dans votre base de code. Dans le diagramme ci-dessus, le seul endroit qui devrait avoir un catchbloc est l' Load Image User Commandendroit où l'erreur est rapportée. Dans l'idéal, rien d'autre ne devrait avoir catchquoi que ce soit car sinon, cela commence à devenir aussi fastidieux et aussi source d'erreurs que la gestion du code d'erreur.

Donc, si vous me demandez si vous avez une base de code qui profite vraiment de la gestion des exceptions de manière élégante, elle devrait avoir un nombre minimum de catchblocs (minimum, je ne veux pas dire zéro, mais plutôt un pour chaque haute unique opération de l'utilisateur final qui pourrait échouer, voire même moins si toutes les opérations d'utilisateur supérieur sont appelées via un système de commande central).

Nettoyage des ressources

Cependant, la gestion des exceptions ne résout que la nécessité d'éviter de traiter manuellement les aspects de la propagation des erreurs liés au flux de contrôle dans des chemins exceptionnels distincts des flux d'exécution normaux. Souvent, une fonction servant de propagateur d’erreur, même si elle le fait automatiquement maintenant avec EH, peut encore acquérir certaines ressources qu’elle doit détruire. Par exemple, une telle fonction peut ouvrir un fichier temporaire qu’elle doit fermer avant de quitter quoi que ce soit, ou verrouiller un mutex qu’elle doit déverrouiller quoi qu’il en soit.

Pour cela, je pourrais appeler la colère de beaucoup de programmeurs de toutes sortes de langages, mais je pense que l’approche C ++ est idéale. Le langage introduit des destructeurs qui sont invoqués de manière déterministe à la sortie d’un objet. Pour cette raison, le code C ++ qui, par exemple, verrouille un mutex via un objet mutex couvert avec un destructeur n'a pas besoin de le déverrouiller manuellement, car il sera automatiquement déverrouillé une fois que l'objet est hors de portée, peu importe ce qui se produit (même s'il s'agit d'une exception). rencontré). Il n’ya donc vraiment pas besoin d’un code C ++ bien écrit pour traiter le nettoyage des ressources locales.

Dans les langues dépourvues de destructeurs, il peut être nécessaire d'utiliser un finallybloc pour nettoyer manuellement les ressources locales. Cela dit, il vaut mieux battre son code avec la propagation manuelle des erreurs, à condition de ne pas avoir à faire d' catchexception dans tous les cas.

Inverser les effets secondaires externes

C'est le problème conceptuel le plus difficile à résoudre. Si une fonction, qu’il s’agisse d’un propagateur d’erreur ou d’un point d’échec, provoque des effets secondaires externes, elle doit alors annuler ou annuler ces effets secondaires pour rétablir le système dans un état identique à celui de l’opération, au lieu de " semi-valide "état où l'opération à mi-parcours a réussi. Je ne connais aucun langage qui rende ce problème conceptuel beaucoup plus facile, à l'exception des langages qui réduisent au minimum le besoin pour la plupart des fonctions de provoquer des effets secondaires externes, tels que les langages fonctionnels qui gravitent autour de l'immuabilité et des structures de données persistantes.

C’est finallyl’une des solutions les plus élégantes au problème des langages axés sur la mutabilité et les effets secondaires, car ce type de logique est très spécifique à une fonction particulière et ne correspond pas si bien au concept de «nettoyage des ressources». ". Et je vous recommande d'utiliser finallylibéralement dans ces cas-là pour vous assurer que votre fonction annule les effets secondaires dans les langues qui le supportent, que vous ayez besoin d'un catchbloc ou non (et encore, si vous me demandez, le code bien écrit devrait avoir le nombre minimum de catchblocs, et tous les catchblocs doivent être à des endroits où cela a le plus de sens comme dans le diagramme ci-dessus Load Image User Command).

Langue de rêve

Cependant, IMO finallyest proche de l’idéal pour le renversement des effets secondaires mais pas tout à fait. Nous devons introduire une booleanvariable pour annuler efficacement les effets secondaires dans le cas d'une sortie prématurée (d'une exception levée ou autre), comme suit:

bool finished = false;
try
{
    // Cause external side effects.
    ...

    // Indicate that all the external side effects were
    // made successfully.
    finished = true; 
}
finally
{
    // If the function prematurely exited before finishing
    // causing all of its side effects, whether as a result of
    // an early 'return' statement or an exception, undo the
    // side effects.
    if (!finished)
    {
        // Undo side effects.
        ...
    }
}

Si je pouvais un jour concevoir un langage, la méthode de rêve pour résoudre ce problème serait d'automatiser le code ci-dessus:

transaction
{
    // Cause external side effects.
    ...
}
rollback
{
    // This block is only executed if the above 'transaction'
    // block didn't reach its end, either as a result of a premature
    // 'return' or an exception.

    // Undo side effects.
    ...
}

... avec des destructeurs pour automatiser le nettoyage des ressources locales, nous en avons donc seulement besoin transaction, rollbacket catch(bien que je puisse encore vouloir ajouter finallypour, par exemple, travailler avec des ressources C qui ne se nettoient pas elles-mêmes). Cependant, finallyavec une booleanvariable est la chose la plus proche de rendre cela simple que j'ai trouvé jusqu'à présent manquant de la langue de mes rêves. La deuxième solution la plus simple que j'ai trouvée pour cela est celle des gardes de la portée dans des langages tels que C ++ et D, mais j'ai toujours trouvé les gardes de la portée un peu gênants sur le plan conceptuel, car cela brouille l'idée de "nettoyage des ressources" et de "retournement des effets secondaires". À mon avis, ce sont des idées très distinctes qui doivent être abordées différemment.

Mon petit rêve de langage s’articulerait également autour de l’immuabilité et des structures de données persistantes, ce qui faciliterait beaucoup, bien que cela ne soit pas nécessaire, l’écriture de fonctions efficaces sans avoir à copier en profondeur d’immenses structures de données dans leur intégralité, même si la fonction pas d'effets secondaires.

Conclusion

Quoi qu'il en soit, à part mes divagations, je pense que votre try/finallycode pour fermer le socket est bon et bon, vu que Python n'a pas l'équivalent C ++ des destructeurs, et je pense personnellement que vous devriez l'utiliser de manière libérale pour les endroits qui doivent inverser leurs effets secondaires. et minimisez le nombre d'endroits où vous devez vous rendre catchà des endroits où cela semble le plus logique.


-66

Il est vivement recommandé de corriger les erreurs / exceptions et de les traiter de manière soignée, même si cela n’est pas obligatoire.

La raison pour laquelle je dis cela est parce que je crois que chaque développeur doit connaître le comportement de sa candidature et s'y attaquer, sinon il n'a pas terminé son travail correctement. Il n'y a pas de situation pour laquelle un bloc try-finally remplace le bloc try-catch-finally.

Je vais vous donner un exemple simple: Supposons que vous ayez écrit le code permettant de télécharger des fichiers sur le serveur sans intercepter des exceptions. Désormais, si le téléchargement échoue pour une raison quelconque, le client ne saura jamais ce qui ne va pas. Mais, si vous avez intercepté l'exception, vous pouvez afficher un message d'erreur clair expliquant ce qui ne va pas et comment l'utilisateur peut y remédier.

Règle d'or: attrapez toujours l'exception, car deviner prend du temps


22
-1: En Java, une clause finally peut être nécessaire pour libérer des ressources (par exemple, fermer un fichier ou libérer une connexion à une base de données). Cela est indépendant de la capacité de gérer une exception.
kevin cline

@ kevincline, il ne demande pas s'il faut utiliser finalement ou non ... Tout ce qu'il demande, c'est si attraper une exception est nécessaire ou non .... Il sait ce qu'il faut essayer, attraper et finalement faire ..... partie la plus essentielle, nous le savons tous et pourquoi il est utilisé ....
Pankaj Upadhyay

63
@Pankaj: votre réponse suggère qu'une catchclause devrait toujours être présente chaque fois qu'il y a un try. Les contributeurs plus expérimentés, y compris moi-même, estiment qu'il s'agit d'un mauvais conseil. Votre raisonnement est imparfait. La méthode contenant le tryn'est pas le seul endroit possible où l'exception peut être interceptée. Il est souvent plus simple et préférable d'autoriser les captures et de signaler les exceptions du plus haut niveau, plutôt que de dupliquer les clauses de capture dans le code.
kevin cline

@ kevincline, je pense que la perception de la question par rapport à d'autres est légèrement différente. La question était précisément de savoir si nous devrions intercepter une exception ou non ... Et j'ai répondu à cet égard. La gestion des exceptions peut se faire de différentes manières et pas seulement essayer-finalement. Mais, ce n'était pas la préoccupation de OP. Si élever une exception est suffisant pour vous et les autres gars, alors je dois dire que je souhaite la meilleure des chances.
Pankaj Upadhyay

Avec des exceptions, vous voulez que l'exécution normale des instructions soit interrompue (et sans vérification manuelle du succès à chaque étape). C'est particulièrement le cas avec les appels de méthode profondément imbriqués - une méthode à 4 couches dans une bibliothèque ne peut pas simplement "avaler" une exception; il doit être rejeté à travers toutes les couches. Bien entendu, chaque couche pourrait encapsuler l'exception et ajouter des informations supplémentaires; cela n'est souvent pas fait pour diverses raisons, le temps supplémentaire nécessaire pour se développer et la verbosité inutile étant les deux plus importantes.
Daniel B
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.