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 catch
bloqué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:
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 NULL
ou 0
ou -1
ou 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 Scanline
pourrait avoir à gérer un échec de malloc
puis retourner une erreur à Convert Scanlines
, puis Convert Scanlines
devra 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 error
de 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 catch
blocs dans votre base de code. Dans le diagramme ci-dessus, le seul endroit qui devrait avoir un catch
bloc est l' Load Image User Command
endroit où l'erreur est rapportée. Dans l'idéal, rien d'autre ne devrait avoir catch
quoi 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 catch
blocs (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 finally
bloc 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' catch
exception 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 finally
l’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 finally
libé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 catch
bloc ou non (et encore, si vous me demandez, le code bien écrit devrait avoir le nombre minimum de catch
blocs, et tous les catch
blocs 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 finally
est proche de l’idéal pour le renversement des effets secondaires mais pas tout à fait. Nous devons introduire une boolean
variable 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
, rollback
et catch
(bien que je puisse encore vouloir ajouter finally
pour, par exemple, travailler avec des ressources C qui ne se nettoient pas elles-mêmes). Cependant, finally
avec une boolean
variable 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/finally
code 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.