De nombreux langages modernes fournissent de riches fonctionnalités de gestion des exceptions , mais le langage de programmation Swift d’Apple ne fournit pas de mécanisme de gestion des exceptions .
Imprégné d'exceptions que je suis, j'ai du mal à comprendre ce que cela signifie. Swift a des assertions, et bien sûr renvoie des valeurs; mais j'ai du mal à imaginer comment ma façon de penser basée sur les exceptions mappe sur un monde sans exceptions (et, d'ailleurs, pourquoi un tel monde est souhaitable ). Y a-t-il des choses que je ne peux pas faire dans une langue comme Swift que je pourrais faire avec des exceptions? Est-ce que je gagne quelque chose en perdant des exceptions?
Comment par exemple pourrais-je mieux exprimer quelque chose comme
try:
operation_that_can_throw_ioerror()
except IOError:
handle_the_exception_somehow()
else:
# we don't want to catch the IOError if it's raised
another_operation_that_can_throw_ioerror()
finally:
something_we_always_need_to_do()
dans une langue (Swift, par exemple) qui ne gère pas les exceptions?
panic
ce qui n’est pas tout à fait pareil. En plus de ce qui est dit là-bas, une exception n’est guère plus qu’un moyen sophistiqué (mais confortable) d’exécuter une actionGOTO
, bien que personne ne l’appelle ainsi, pour des raisons évidentes.Réponses:
Dans la programmation intégrée, les exceptions étaient traditionnellement interdites, car les frais généraux liés à la décompression de la pile étaient considérés comme une variabilité inacceptable lorsqu’on essayait de maintenir les performances en temps réel. Bien que les smartphones puissent techniquement être considérés comme des plates-formes temps réel, ils sont suffisamment puissants pour permettre aux anciennes limitations des systèmes embarqués de ne plus être appliquées. Je soulève juste la question dans un souci de minutie.
Les exceptions sont souvent prises en charge dans les langages de programmation fonctionnels, mais si rarement utilisées qu'elles pourraient ne pas l'être. Une des raisons est l'évaluation paresseuse, qui est effectuée occasionnellement même dans des langues qui ne sont pas paresseuses par défaut. Avoir une fonction qui s'exécute avec une pile différente de celle où il était en file d'attente, il est difficile de déterminer où placer votre gestionnaire d'exceptions.
L'autre raison est que les fonctions de première classe permettent des constructions telles que des options et des futures qui vous offrent les avantages syntaxiques des exceptions avec plus de flexibilité. En d'autres termes, le reste du langage est suffisamment expressif pour que les exceptions ne vous rapportent rien.
Je ne connais pas bien Swift, mais le peu que j'ai lu au sujet de sa gestion des erreurs donne à penser qu'ils étaient destinés à la gestion des erreurs afin de suivre des modèles de style plus fonctionnels. J'ai vu des exemples de code avec
success
et desfailure
blocs qui ressemblent beaucoup à des futurs.Voici un exemple utilisant un
Future
de ce tutoriel Scala :Vous pouvez voir qu'il a à peu près la même structure que votre exemple utilisant des exceptions. Le
future
bloc est comme untry
. LeonFailure
bloc est comme un gestionnaire d'exceptions. En Scala, comme dans la plupart des langages fonctionnels,Future
est complètement implémenté en utilisant le langage lui-même. Cela ne nécessite aucune syntaxe spéciale, contrairement aux exceptions. Cela signifie que vous pouvez définir vos propres constructions similaires. Peut-être ajouter untimeout
bloc, par exemple, ou une fonctionnalité de journalisation.De plus, vous pouvez faire passer le futur, le renvoyer depuis la fonction, le stocker dans une structure de données ou autre. C'est une valeur de première classe. Vous n'êtes pas limité comme des exceptions qui doivent être propagées directement dans la pile.
Les options résolvent le problème de gestion des erreurs d'une manière légèrement différente, ce qui fonctionne mieux pour certains cas d'utilisation. Vous n'êtes pas coincé avec la seule méthode.
Voilà le genre de choses que vous "gagnez en perdant des exceptions".
la source
Future
s’agit donc essentiellement d’une manière d’examiner la valeur renvoyée par une fonction, sans attendre de l’attendre. Comme Swift, il est basé sur une valeur de retour, mais contrairement à Swift, la réponse à la valeur de retour peut avoir lieu ultérieurement (un peu comme des exceptions). Droite?Future
bien, mais je pense que vous pourriez mal interpréter Swift. Voir la première partie de cette réponse stackoverflow , par exemple.Either
serait un meilleur exemple IMHOLes exceptions peuvent rendre le code plus difficile à raisonner. Bien qu'ils ne soient pas aussi puissants que les gotos, ils peuvent causer nombre des mêmes problèmes en raison de leur nature non locale. Par exemple, disons que vous avez un morceau de code impératif comme ceci:
Vous ne pouvez pas dire immédiatement si l'une de ces procédures peut générer une exception. Vous devez lire la documentation de chacune de ces procédures pour comprendre cela. (Certaines langues rendent cela légèrement plus facile en augmentant la signature de type avec ces informations.) Le code ci-dessus se compilera parfaitement, que l'une des procédures jette ou non, ce qui rend vraiment facile d'oublier de gérer une exception.
De plus, même si l’intention est de propager l’exception à l’appelant, il est souvent nécessaire d’ajouter du code supplémentaire pour éviter que les éléments restent incohérents (par exemple, si votre cafetière tombe en panne, vous devez tout de même nettoyer le gâchis et le renvoyer. la tasse!). Ainsi, dans de nombreux cas, le code qui utilise des exceptions semble aussi complexe que le code qui ne l’était pas en raison du nettoyage supplémentaire requis.
Les exceptions peuvent être émulées avec un système de types suffisamment puissant. La plupart des langages qui évitent les exceptions utilisent des valeurs de retour pour obtenir le même comportement. Cela ressemble à la façon de faire en C, mais les systèmes de type modernes le rendent généralement plus élégant et plus difficile à oublier pour gérer la condition d'erreur. Ils peuvent également fournir du sucre syntaxique pour rendre les choses moins lourdes, parfois presque aussi propres qu’elles le seraient avec des exceptions.
En particulier, en intégrant la gestion des erreurs dans le système de types plutôt que de la mettre en œuvre séparément, les "exceptions" peuvent être utilisées pour d’autres tâches qui ne sont même pas liées aux erreurs. (Il est bien connu que la gestion des exceptions est en réalité une propriété des monades.)
la source
goto
: Legoto
est limité à une seule fonction, ce qui est une gamme assez petite fonction fournie sont vraiment petits, l'exception agit plus comme une croixgoto
etcome from
(voir en.wikipedia.org/wiki/INTERCAL ;-)). Il peut très bien connecter deux morceaux de code, en sautant éventuellement du code pour certaines fonctions tierces. La seule chose qu’il ne peut pas faire, quigoto
peut, est de revenir en arrière.Il y a d'excellentes réponses ici, mais je pense qu'une raison importante n'a pas été suffisamment soulignée: lorsque des exceptions se produisent, les objets peuvent être laissés dans des états non valides. Si vous pouvez "attraper" une exception, votre code de gestionnaire d'exceptions pourra accéder à ces objets non valides et les utiliser. Cela ira terriblement mal à moins que le code de ces objets soit écrit parfaitement, ce qui est très, très difficile à faire.
Par exemple, imaginez la mise en œuvre de Vector. Si quelqu'un instancie votre vecteur avec un ensemble d'objets, mais qu'une exception se produise lors de l'initialisation (par exemple, lors de la copie de vos objets dans la mémoire nouvellement allouée), il est très difficile de coder correctement l'implémentation de vecteur de manière à la mémoire est une fuite. Ce court article de Stroustroup couvre l'exemple de Vector .
Et ce n'est que la pointe de l'iceberg. Et si, par exemple, vous aviez copié certains des éléments, mais pas tous les éléments? Pour implémenter correctement un conteneur tel que Vector, vous devez presque rendre chaque action réversible, de sorte que l'opération est entièrement atomique (comme une transaction de base de données). C'est compliqué et la plupart des applications se trompent. Et même lorsque cela est fait correctement, cela complique grandement le processus de mise en œuvre du conteneur.
Donc, certaines langues modernes ont décidé que cela n'en valait pas la peine. Rust, par exemple, a des exceptions, mais elles ne peuvent pas être "interceptées". Il est donc impossible pour le code d'interagir avec des objets dans un état non valide.
la source
Une des choses qui m'a le plus surpris au début, c'est que le langage Rust ne prend pas en charge les exceptions de capture. Vous pouvez générer des exceptions, mais seul le moteur d'exécution peut les intercepter lorsqu'une tâche (pensez à un thread, mais pas toujours à un thread séparé du système d'exploitation) meurt; si vous démarrez une tâche vous-même, vous pouvez alors demander si elle se termine normalement ou si elle se
fail!()
termine.En tant que tel, il n’est pas idiomatique de le faire
fail
très souvent. Les rares cas où cela se produit sont, par exemple, dans le harnais de test (qui ne sait pas à quoi ressemble le code utilisateur), en tant que niveau supérieur d'un compilateur (la plupart des compilateurs divisent à la place), ou lors de l'appel d'un rappel sur l'entrée de l'utilisateur.Au lieu de cela, le langage courant consiste à utiliser le
Result
modèle pour ignorer explicitement les erreurs à traiter. Ceci est considérablement facilité par latry!
macro , qui peut être enroulée autour de toute expression générant un résultat et le bras réussi s'il en existe un, ou renvoyant autrement de la fonction.la source
assert
, mais noncatch
?À mon avis, les exceptions sont un outil essentiel pour détecter les erreurs de code au moment de l'exécution. Tant dans les tests et dans la production. Faites en sorte que leurs messages soient suffisamment détaillés pour que, combiné à une trace de pile, vous puissiez savoir ce qui s’est passé dans un journal.
Les exceptions constituent principalement un outil de développement et un moyen d'obtenir des rapports d'erreur raisonnables de la production dans des cas inattendus.
Mis à part la séparation des problèmes (chemin heureux avec les erreurs attendues seulement, jusqu'à atteindre un gestionnaire générique d'erreurs inattendues), il est intéressant de rendre votre code plus lisible et maintenable, il est en fait impossible de préparer votre code pour tout ce qui est possible. cas inattendus, même en le gonflant avec un code de traitement des erreurs pour compléter l’illisibilité.
C'est en fait le sens du mot "inattendu".
Au fait, ce qui est attendu et ce qui ne l'est pas est une décision qui ne peut être prise que sur le site de l'appel. C'est pourquoi les exceptions vérifiées dans Java n'ont pas fonctionné - la décision est prise au moment de développer une API, quand il est difficile de savoir ce qui est attendu ou inattendu.
Exemple simple: l'API d'une carte de hachage peut avoir deux méthodes:
et
le premier levant une exception s'il n'est pas trouvé, le dernier vous donnant une valeur optionnelle. Dans certains cas, ce dernier a plus de sens, mais dans d’autres, votre code doit absolument s’attendre à ce qu’il y ait une valeur pour une clé donnée. Par conséquent, s’il n’en existe pas, il s’agit d’une erreur que ce code ne peut corriger l'hypothèse a échoué. Dans ce cas, le comportement souhaité est de sortir du chemin du code et de passer à un gestionnaire générique en cas d'échec de l'appel.
Code ne doit jamais essayer de traiter des hypothèses de base qui ont échoué.
Sauf en les vérifiant et en jetant des exceptions bien lisibles, bien sûr.
Lancer des exceptions n'est pas mauvais, mais les attraper peut l'être. N'essayez pas de réparer des erreurs inattendues. Attrapez les exceptions dans quelques endroits où vous souhaitez continuer une boucle ou une opération, enregistrez-les, signalez peut-être une erreur inconnue, et c'est tout.
Les blocs de prise partout sont une très mauvaise idée.
Concevez vos API de manière à ce qu'il soit facile d'exprimer votre intention, c'est-à-dire d'indiquer si vous attendez un cas particulier, comme une clé non trouvée ou non. Les utilisateurs de votre API peuvent alors choisir l'appel de lancement uniquement pour des cas vraiment inattendus.
J'imagine que les mauvaises expériences sont la raison pour laquelle les gens refusent les exceptions et vont trop loin en omettant cet outil essentiel d'automatisation de la gestion des erreurs et de la séparation des préoccupations des nouvelles langues.
Cela, et certains malentendus sur ce qu’ils sont réellement bons pour.
Les simuler en faisant TOUT avec la liaison monadique rend votre code moins lisible et moins maintenable, et vous vous retrouvez sans trace de pile, ce qui rend cette approche bien pire.
La gestion des erreurs de style fonctionnel est idéale pour les cas d'erreur prévus.
Laissez la gestion des exceptions s'occuper automatiquement de tout le reste, c'est pour ça :)
la source
Swift utilise ici les mêmes principes que Objective-C, mais plus en conséquence. En Objective-C, les exceptions indiquent des erreurs de programmation. Ils ne sont pas gérés sauf par les outils de signalement des incidents. La "gestion des exceptions" se fait en corrigeant le code. (Il y a quelques très mauvaises exceptions. Par exemple, dans les communications entre processus. Mais c'est assez rare et beaucoup de gens ne le rencontrent jamais. Et Objective-C a effectivement try / catch / finally / throw, mais vous les utilisez rarement). Swift vient d'éliminer la possibilité d'attraper des exceptions.
Swift a une fonctionnalité qui ressemble à la gestion des exceptions mais est simplement appliquée à la gestion des erreurs. Historiquement, Objective-C avait un modèle de traitement des erreurs assez répandu: une méthode retournait un BOOL (YES en cas de succès) ou une référence d'objet (nil en cas d'échec, pas de succès) et avait un paramètre "pointeur vers NSError *" qui serait utilisé pour stocker une référence NSError. Swift convertit automatiquement les appels à une telle méthode en quelque chose qui ressemble à la gestion des exceptions.
En général, les fonctions Swift peuvent facilement renvoyer des alternatives, comme un résultat si une fonction fonctionnait correctement et une erreur en cas d'échec. cela facilite beaucoup le traitement des erreurs. Mais la réponse à la question de départ: les concepteurs de Swift pensaient évidemment que créer un langage sûr et écrire du code réussi dans un tel langage était plus facile si le langage ne contenait pas d'exceptions.
la source
En C, vous vous retrouvez avec quelque chose comme ce qui précède.
Non, il n'y a rien. Vous finissez juste par gérer les codes de résultats au lieu d'exceptions.
Les exceptions vous permettent de réorganiser votre code afin que la gestion des erreurs soit distincte de votre code de chemin d'accès, mais c'est à peu près tout.
la source
...throw_ioerror()
renvoyer des erreurs plutôt que de lancer des exceptions?Result<T, E>
énumération, qui peut être un typeOk<T>
ou un typeErr<E>
, avecT
le type souhaité, le cas échéant, etE
un type représentant une erreur. La correspondance des modèles et certaines méthodes particulières simplifient le traitement des succès et des échecs. En bref, ne présumez pas que l'absence d'exceptions signifie automatiquement des codes d'erreur.En plus de la réponse de Charlie:
Ces exemples de traitement des exceptions déclarées que vous voyez dans de nombreux manuels et livres ne sont très intelligents que sur de très petits exemples.
Même si vous mettez de côté l'argument concernant l'état d'objet invalide, ils provoquent toujours une douleur vraiment énorme lorsqu'il s'agit d'une application volumineuse.
Par exemple, lorsque vous devez traiter des opérations d'E / S, en utilisant une cryptographie, vous pouvez avoir 20 types d'exceptions qui peuvent être rejetées sur 50 méthodes. Imaginez la quantité de code de gestion des exceptions dont vous aurez besoin. La gestion des exceptions prendra plusieurs fois plus de code que le code lui-même.
En réalité, vous savez que des exceptions ne peuvent pas apparaître et que vous n'avez jamais besoin d'écrire autant de tâches de gestion des exceptions. Vous devez donc utiliser certaines solutions pour ignorer les exceptions déclarées. Dans ma pratique, il ne faut gérer que 5% environ des exceptions déclarées pour avoir une application fiable.
la source