web-dev-qa-db-fra.com

Pourquoi utiliser try… enfin sans clause catch?

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

Dans Python ce qui suit semble légal et peut avoir du sens:

try:
  #do work
finally:
  #do something unconditional

Cependant, le code n'a rien catch rien. De même, on pourrait penser en Java ce serait comme suit:

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

Cela a l'air bien et tout à coup, je n'ai pas à me soucier des types d'exceptions, etc. Si c'est une bonne pratique, quand est-ce une bonne pratique? 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é la source. Je pose des questions à ce sujet car il pourrait s'agir d'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 elle doit retourner quelque chose. Cependant, il peut se trouver dans un endroit qui ne doit pas être atteint et doit être un point de retour. Donc, même si je gère les exceptions ci-dessus, je renvoie toujours NULL ou une chaîne vide à un moment donné du code qui ne devrait pas être atteint, souvent la fin de la méthode/fonction. J'ai toujours réussi à restructurer le code pour qu'il n'ait pas à return NULL, car cela ne semble absolument pas être une bonne pratique.

82
Niklas

Cela dépend si vous pouvez gérer les exceptions qui peuvent être déclenché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 son origine.

Si vous ne pouvez pas les gérer localement, il suffit d'avoir un try / finally block est parfaitement raisonnable - en supposant qu'il y ait du code que vous devez exécuter, que la méthode réussisse ou non. Par exemple (extrait de 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 de try { } finally { }, en utilisant la clause finally pour garantir que le flux est fermé, quel que soit le succès ou l'échec de la lecture.

Cependant, vous aurez toujours besoin d'un gestionnaire d'exceptions quelque part dans votre code - sauf si vous voulez que votre application plante complètement bien sûr. Cela dépend de l'architecture de votre application où se trouve exactement ce gestionnaire.

148
ChrisF

Le bloc finally est utilisé pour le code qui doit toujours s'exécuter, qu'une condition d'erreur (exception) se soit produite ou non.

Le code du bloc finally est exécuté une fois le bloc try terminé et, si une exception interceptée s'est produite, une fois le bloc catch correspondant terminé. Il est toujours exécuté, même si une exception non interceptée s'est produite dans le bloc try ou catch.

Le bloc finally est généralement utilisé pour fermer des fichiers, des connexions réseau, etc. qui ont été ouverts dans le bloc try. La raison en est que le fichier ou la connexion réseau doit être fermé, que l'opération utilisant ce fichier ou cette connexion réseau ait réussi ou échoué.

Des précautions doivent être prises dans le bloc finally pour s'assurer qu'il ne lève pas lui-même une exception. Par exemple, assurez-vous de vérifier toutes les variables pour null, etc.

36
yfeldblum

Un exemple où essayez ... enfin sans clause catch est approprié (et encore plus, idiomatique ) in = Java est l'utilisation de Lock dans le package de verrous des utilitaires simultanés.

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

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

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

    Lorsque le verrouillage et le déverrouillage se produisent dans des étendues différentes, il faut veiller à ce que tout le code exécuté pendant que le verrou soit maintenu soit protégé par try-finally ou try-catch pour garantir est libéré si nécessaire .

17
gnat

Au niveau de base, catch et finally résolvent 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 que le code actuel a créées/modifiées, peu importe si un problème est survenu 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 bloc finallydoit ê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 mis à un niveau différent dans l'appel empiler.

Cependant, le catch est différent: l'emplacement correct dépend de où vous pouvez réellement gérer l'exception. Il n'est pas utile d'attraper une exception à un endroit où vous ne pouvez rien y faire, il est donc parfois préférable de simplement la laisser passer.

12
Joachim Sauer

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

En C++, il utilise RAII et des constructeurs/destructeurs; en Python c'est une instruction with; et en C #, c'est une instruction using.

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

6
Neil G

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

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

Ce qui libère les ressources quelle que soit la façon dont la méthode a été terminée avec une exception ou une instruction de retour régulière.

Que ce soit bon ou mauvais est à débattre, mais try {} finally {} n'est pas toujours limité à la gestion des exceptions.

4
Kamiel Wanrooij

Je pourrais invoquer la colère des Pythonistas (je ne sais pas car je n'utilise pas Python beaucoup) ou des programmeurs d'autres langues avec cette réponse, mais à mon avis la plupart les fonctions devraient pas avoir un bloc catch, idéalement parlant. Pour montrer pourquoi, permettez-moi de comparer cela à la propagation manuelle de code d'erreur du type que je devais faire lorsque je travaillais avec Turbo C à la fin des années 80 et au début des années 90.

Disons donc que nous avons une fonction pour charger une image ou quelque chose comme ça en réponse à un utilisateur sélectionnant un fichier image à charger, et cela est écrit en C et Assembly:

enter image description here

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 matière de 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 que throw, c'est-à-dire) et les fonctions "récupération d'erreur et rapport" (celles que catch, c'est à dire).

Ces fonctions étaient toujours triviales à écrire correctement avant que la gestion des exceptions ne soit disponible car une fonction qui peut rencontrer un échec externe, comme l'échec de l'allocation de mémoire, peut simplement renvoyer un NULL ou 0 ou -1 ou définissez un code d'erreur global ou quelque chose à cet effet. Et la récupération/rapport d'erreurs a toujours été facile car une fois que vous avez parcouru la pile d'appels à un point où il était logique de récupérer et de signaler les échecs, il vous suffit de prendre le code d'erreur et/ou le message et de le signaler à l'utilisateur. Et naturellement une fonction à la feuille de cette hiérarchie qui ne pourra jamais, jamais échouer, peu importe comment elle changera à l'avenir (Convert Pixel) est très simple à écrire correctement (au moins en ce qui concerne la gestion des erreurs).

Propagation des erreurs

Cependant, les fonctions fastidieuses sujettes à l'erreur humaine étaient les propagateurs d'erreur, celles qui ne se sont pas directement soldées par un échec mais ont appelé des fonctions qui pourraient échouer quelque part plus profondément dans la hiérarchie. À ce moment, Allocate Scanline devra peut-être gérer un échec de malloc, puis retourner une erreur à Convert Scanlines, puis Convert Scanlines devrait vérifier cette erreur et la transmettre à Decompress Image, puis Decompress Image->Parse Image, et Parse Image->Load Image, et Load Image à la commande utilisateur où l'erreur est finalement signalée.

C'est là que de nombreux humains font des erreurs, car il suffit d'un seul propagateur d'erreurs pour ne pas 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 codes d'erreur sont renvoyés par des fonctions, nous perdons à peu près la capacité, par exemple, dans 90% de notre base de code, de renvoyer des valeurs intéressantes sur succès car tant de fonctions devraient réserver leur valeur de retour pour retourner un code d'erreur en cas d'échec.

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

Alors, comment pouvons-nous réduire la possibilité d'erreur humaine? Ici, je pourrais même invoquer la colère de certains programmeurs C, mais une amélioration immédiate à mon avis est d'utiliser des codes d'erreur global, comme OpenGL avec glGetError. Cela libère au moins les fonctions pour retourner des valeurs significatives d'intérêt 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 y a aussi des cas où une fonction peut rencontrer une erreur mais il est relativement inoffensif qu'elle continue 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 avoir à vérifier les erreurs par rapport à 90% des appels de fonction effectués dans chaque fonction, de sorte qu'il peut toujours permettre une gestion correcte des erreurs sans être aussi méticuleux.

Réduction des erreurs humaines: gestion des exceptions

Cependant, la solution ci-dessus nécessite toujours autant de fonctions pour gérer l'aspect du flux de contrôle de la propagation manuelle des erreurs, même si elle a pu réduire le nombre de lignes de _ if error happened, return error type de code. Cela ne l'éliminerait pas complètement car il faudrait encore souvent au moins un endroit pour vérifier une erreur et renvoyer pour presque chaque fonction de propagation d'erreur. C'est donc lorsque la gestion des exceptions entre en jeu pour sauver la situation (sorta).

Mais la valeur de la gestion des exceptions ici est de libérer le besoin de traiter l'aspect du flux de contrôle de la propagation manuelle des erreurs. Cela signifie que sa valeur est liée à la possibilité d'éviter d'écrire une cargaison de blocs catch dans votre base de code. Dans le diagramme ci-dessus, le seul endroit qui devrait avoir un bloc catch est le Load Image User Command où l'erreur est signalée. Rien d'autre ne devrait idéalement avoir à catch quoi que ce soit, sinon cela commence à devenir aussi fastidieux et sujet aux erreurs que la gestion des codes d'erreur.

Donc, si vous me demandez, si vous avez une base de code qui bénéficie vraiment de la gestion des exceptions d'une manière élégante, elle devrait avoir le minimum nombre de blocs catch (au minimum, je ne signifie pas zéro, mais plutôt un pour chaque opération utilisateur haut de gamme unique qui pourrait échouer, et peut-être même moins si toutes les opérations utilisateur haut de gamme sont appelées via un système de commande central).

Nettoyage des ressources

Cependant, la gestion des exceptions ne résout que le besoin d'éviter de traiter manuellement les aspects du flux de contrôle de la propagation des erreurs dans des chemins exceptionnels séparés des flux normaux d'exécution. Souvent, une fonction qui sert de propagateur d'erreurs, même si elle le fait automatiquement maintenant avec EH, peut toujours acquérir certaines ressources dont elle a besoin pour détruire. Par exemple, une telle fonction peut ouvrir un fichier temporaire qu'elle doit fermer avant de revenir de la fonction, ou verrouiller un mutex dont elle a besoin pour se déverrouiller quoi qu'il arrive.

Pour cela, je pourrais invoquer la colère de nombreux programmeurs de toutes sortes de langages, mais je pense que l'approche C++ est idéale. Le langage introduit destructeurs qui sont invoqués de façon déterministe à l'instant où un objet sort du cadre. Pour cette raison, le code C++ qui, par exemple, verrouille un mutex via un objet mutex de portée avec un destructeur n'a pas besoin de le déverrouiller manuellement, car il sera automatiquement déverrouillé une fois que l'objet sera hors de portée quoi qu'il arrive (même si une exception est rencontré). Il n'est donc vraiment pas nécessaire qu'un code C++ bien écrit ait à gérer le nettoyage des ressources locales.

Dans les langues dépourvues de destructeurs, ils peuvent avoir besoin d'utiliser un bloc finally pour nettoyer manuellement les ressources locales. Cela dit, il vaut toujours mieux jeter votre code avec une propagation d'erreur manuelle fourni vous n'avez pas à catch exceptions partout dans le monde.

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'erreurs ou d'un point de défaillance, provoque des effets secondaires externes, elle doit alors annuler ou "annuler" ces effets secondaires pour remettre le système dans un état comme si l'opération n'avait jamais eu lieu, au lieu d'un " semi-valide "état où l'opération a réussi à mi-chemin. Je ne connais aucun langage qui rende ce problème conceptuel beaucoup plus facile, sauf les langages qui réduisent simplement la nécessité pour la plupart des fonctions de provoquer des effets secondaires externes en premier lieu, comme les langages fonctionnels qui tournent autour de l'immuabilité et des structures de données persistantes.

Ici, finally est sans doute l'une des solutions les plus élégantes au problème des langages autour de la mutabilité et des effets secondaires, car souvent ce type de logique est très spécifique à une fonction particulière et ne correspond pas si bien à le concept de "nettoyage des ressources". Et je recommande d'utiliser généreusement finally dans ces cas pour vous assurer que votre fonction inverse les effets secondaires dans les langages qui la prennent en charge, que vous ayez ou non besoin d'un bloc catch (et encore, si vous le demandez moi, un code bien écrit devrait avoir le nombre minimum de blocs catch, et tous les blocs catch devraient être dans les endroits où cela a le plus de sens comme avec le diagramme ci-dessus dans Load Image User Command).

Langue de rêve

Cependant, l'OMI finally est proche de l'idéal pour l'inversion des effets secondaires, mais pas tout à fait. Nous devons introduire une variable boolean pour annuler efficacement les effets secondaires en cas de sortie prématurée (d'une exception levée ou autre), comme ceci:

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 jamais concevoir un langage, ma façon rêvée de résoudre ce problème serait comme ceci pour 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, ce qui fait que nous n'avons besoin que de transaction, rollback et catch (bien que je veuille encore ajouter finally pour, disons, travailler avec des ressources C qui ne se nettoient pas). Cependant, finally avec une variable boolean est la chose la plus proche de rendre cela simple que j'ai trouvé jusqu'à présent sans le langage de mes rêves. La deuxième solution la plus simple que j'ai trouvée pour cela est scope guards dans des langages comme C++ et D, mais j'ai toujours trouvé les gardes de portée un peu maladroits conceptuellement car cela brouille l'idée de "nettoyage des ressources" "et" inversion des effets secondaires ". À mon avis, ce sont des idées très distinctes à aborder différemment.

Mon petit rêve de langage tournerait également fortement autour de l'immuabilité et des structures de données persistantes pour faciliter l'écriture de fonctions efficaces qui n'ont pas besoin de copier en profondeur des structures de données massives dans leur intégralité même si la fonction provoque pas d'effets secondaires.

Conclusion

Donc de toute façon, avec mes divagations de côté, je pense que votre try/finally le code pour fermer le socket est très bien vu que Python n'a pas l'équivalent C++ des destructeurs, et je pense personnellement que vous devriez l'utiliser généreusement pour les endroits qui doivent inverser les effets secondaires et minimisez le nombre d'endroits où vous devez catch aux endroits où cela a le plus de sens.

1
user204677