web-dev-qa-db-fra.com

Pourquoi concevoir un langage moderne sans mécanisme de gestion des exceptions?

Beaucoup de langages modernes fournissent une gestion riche des exceptions fonctionnalités, mais le Swift d'Apple ne fournit pas de mécanisme de gestion des exceptions .

Imprégné d'exceptions comme je le 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 correspond à 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 manque de gestion des exceptions?

48
orome

Dans la programmation intégrée, les exceptions n'étaient traditionnellement pas autorisées, car la surcharge du déroulement de la pile que vous devez effectuer était considérée comme une variabilité inacceptable lorsque vous essayez de maintenir des performances en temps réel. Bien que les smartphones puissent techniquement être considérés comme des plates-formes en temps réel, ils sont maintenant assez puissants là où les anciennes limitations des systèmes embarqués ne s'appliquent plus vraiment. J'en parle simplement par souci de rigueur.

Les exceptions sont souvent prises en charge dans les langages de programmation fonctionnels, mais si rarement utilisées qu'elles peuvent tout aussi bien 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. Le fait d'avoir une fonction qui s'exécute avec une pile différente de l'endroit où elle a été mise en file d'attente pour exécuter rend difficile de déterminer où placer votre gestionnaire d'exceptions.

L'autre raison est que les fonctions de première classe permettent des constructions comme les options et les futurs 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 Swift, mais le peu que j'ai lu sur sa gestion des erreurs suggère qu'ils étaient destinés à la gestion des erreurs pour suivre des modèles de style plus fonctionnel. J'ai vu des exemples de code avec des blocs success et failure qui ressemblent beaucoup à des futurs.

Voici un exemple utilisant un Future de this Scala tutorial :

val f: Future[List[String]] = future {
  session.getRecentPosts
}
f onFailure {
  case t => println("An error has occured: " + t.getMessage)
}
f onSuccess {
  case posts => for (post <- posts) println(post)
}

Vous pouvez voir qu'il a à peu près la même structure que votre exemple en utilisant des exceptions. Le bloc future est comme un try. Le bloc onFailure est comme un gestionnaire d'exceptions. Dans Scala, comme dans la plupart des langages fonctionnels, Future est implémenté complètement en utilisant le langage lui-même. Il ne nécessite aucune syntaxe spéciale comme le font les exceptions. Cela signifie que vous pouvez définir vos propres constructions similaires. Par exemple, ajoutez un bloc timeout ou une fonctionnalité de journalisation.

De plus, vous pouvez transmettre le futur, le renvoyer de 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 les exceptions qui doivent se propager 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 dans certains cas d'utilisation. Vous n'êtes pas coincé avec une seule méthode.

Voilà le genre de choses que vous "gagnez en perdant des exceptions".

34
Karl Bielefeldt

Les exceptions peuvent rendre le code plus difficile à raisonner. Bien qu'elles ne soient pas aussi puissantes que les gotos, elles peuvent causer bon 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 celui-ci:

cleanMug();
brewCoffee();
pourCoffee();
drinkCoffee();

Vous ne pouvez pas dire en un coup d'œil si l'une de ces procédures peut lever une exception. Vous devez lire la documentation de chacune de ces procédures pour comprendre cela. (Certains langages rendent cela un peu plus facile en augmentant la signature de type avec ces informations.) Le code ci-dessus se compilera très bien, que les procédures soient lancées, 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 faut souvent ajouter du code supplémentaire pour éviter que les choses ne soient laissées dans un état incohérent (par exemple, si votre cafetière casse, vous devez toujours nettoyer le gâchis et revenir la tasse!). Ainsi, dans de nombreux cas, le code qui utilise des exceptions semblerait aussi complexe que le code qui ne l'a pas fait en raison du nettoyage supplémentaire requis.

Les exceptions peuvent être émulées avec un système de type suffisamment puissant. Beaucoup de langages qui évitent les exceptions utilisent des valeurs de retour pour obtenir le même comportement. C'est similaire à la façon dont cela se fait en C, mais les systèmes de type modernes le rendent généralement plus élégant et plus difficile à oublier de 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 à quelques exceptions près.

En particulier, en incorporant la gestion des erreurs dans le système de type plutôt que de l'implémenter comme une fonctionnalité distincte, les "exceptions" peuvent être utilisées pour d'autres choses qui ne sont même pas liées aux erreurs. (Il est bien connu que la gestion des exceptions est en fait une propriété des monades.)

21
Rufflewind

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 invalides. Si vous pouvez "intercepter" une exception, votre code de gestionnaire d'exceptions pourra accéder à ces objets non valides et les utiliser. Cela va mal tourner à moins que le code de ces objets ne soit parfaitement écrit, ce qui est très, très difficile à faire.

Par exemple, imaginez mettre en œuvre Vector. Si quelqu'un instancie votre Vector avec un ensemble d'objets, mais une exception se produit lors de l'initialisation (peut-être, 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 Vector de manière à ce qu'aucun la mémoire a fui. 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 éléments, mais pas tous? Pour implémenter correctement un conteneur comme Vector, vous devez presque rendre toutes les actions que vous effectuez réversibles, donc toute l'opération est atomique (comme une transaction de base de données). C'est compliqué et la plupart des applications se trompent. Et même lorsqu'elle est effectuée correctement, elle complique considérablement le processus de mise en œuvre du conteneur.

Certaines langues modernes ont donc 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 n'y a donc aucun moyen pour le code d'interagir avec des objets dans un état non valide.

14
Charlie Flowers

À mon avis, les exceptions sont un outil essentiel pour détecter les erreurs de code au moment de l'exécution. Tant en tests qu'en production. Faites en sorte que leurs messages soient suffisamment verbeux pour qu'en combinaison avec une trace de pile, vous puissiez comprendre ce qui s'est passé à partir d'un journal.

Les exceptions sont 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 préoccupations (chemin heureux avec seulement les erreurs attendues par rapport à l'échec jusqu'à atteindre un gestionnaire générique pour les erreurs inattendues) étant une bonne chose, rendant votre code plus lisible et maintenable, il est en fait impossible de préparer votre code pour tous les possibles des cas inattendus, même en le gonflant de code de gestion des erreurs pour compléter l'illisibilité.

C'est en fait le sens de "inattendu".

Btw., 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 on ne sait pas du tout ce qui est attendu ou inattendu.

Exemple simple: l'API d'une carte de hachage peut avoir deux méthodes:

Value get(Key)

et

Option<Value> getOption(key)

le premier lançant une exception s'il n'est pas trouvé, le second vous donnant une valeur facultative. Dans certains cas, ce dernier est plus logique, mais dans d'autres, votre code doit s'attendre à ce qu'il y ait une valeur pour une clé donnée, donc s'il n'y en a pas, c'est une erreur que ce code ne peut pas corriger car une base l'hypothèse a échoué. Dans ce cas, il s'agit en fait du comportement souhaité de sortir du chemin de code et de tomber sur un gestionnaire générique en cas d'échec de l'appel.

Le code ne doit jamais essayer de traiter les hypothèses de base qui ont échoué.

Sauf en les vérifiant et en lançant des exceptions bien lisibles, bien sûr.

Lancer des exceptions n'est pas mal, mais les attraper peut l'être. N'essayez pas de corriger des erreurs inattendues. Détectez les exceptions à 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 capture partout sont une très mauvaise idée.

Concevez vos API d'une manière qui permet d'exprimer facilement votre intention, c'est-à-dire de déclarer si vous vous attendez à un certain cas, comme une clé non trouvée ou non. Les utilisateurs de votre API peuvent alors choisir l'appel de lancement uniquement pour les cas vraiment inattendus.

Je suppose que la raison pour laquelle les gens sont mécontents des exceptions et vont trop loin en omettant cet outil crucial pour l'automatisation du traitement des erreurs et une meilleure séparation des préoccupations des nouveaux langages sont de mauvaises expériences.

Cela, et certains malentendus sur ce pour quoi ils sont réellement bons.

Les simuler en faisant TOUT via une liaison monadique rend votre code moins lisible et 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 attendus.

Laissez la gestion des exceptions s'occuper automatiquement de tout le reste, c'est à ça qu'elle sert :)

6
yeoman

Une chose qui m'a initialement surpris à propos du langage Rust est qu'il ne prend pas en charge les exceptions de capture. Vous pouvez lever des exceptions, mais seul le runtime peut les détecter lorsqu'une tâche (pensez thread, mais pas toujours un thread OS séparé) meurt; si vous démarrez une tâche vous-même, vous pouvez demander si elle s'est terminée normalement ou si elle fail!()ed.

En tant que tel, il n'est pas idiomatique de fail très souvent. Les quelques cas où cela se produit sont, par exemple, dans le faisceau de test (qui ne sait pas à quoi ressemble le code utilisateur), comme le niveau supérieur d'un compilateur (la plupart des compilateurs fork à la place), ou lors de l'appel d'un rappel sur l'entrée utilisateur.

Au lieu de cela, l'idiome commun est d'utiliser le modèle Result pour transmettre explicitement les erreurs qui doivent être gérées. Ceci est rendu beaucoup plus facile par le try! macro , qui peut être enroulé autour de toute expression qui donne un résultat et donne le bras réussi s'il y en a un, ou sinon revient tôt de la fonction.

use std::io::IoResult;
use std::io::File;

fn load_file(name: &Path) -> IoResult<String>
{
    let mut file = try!(File::open(name));
    let s = try!(file.read_to_string());
    return Ok(s);
}

fn main()
{
    print!("{}", load_file(&Path::new("/tmp/hello")).unwrap());
}
6
o11c

Swift utilise ici les mêmes principes qu'Objective-C, mais plus en conséquence. Dans Objective-C, les exceptions indiquent des erreurs de programmation. Ils ne sont gérés que par des outils de rapport de plantage. La "gestion des exceptions" se fait en corrigeant le code. (Il y a quelques exceptions ahem. Par exemple dans les communications inter-processus. Mais c'est assez rare et beaucoup de gens ne s'y rencontrent jamais. Et Objective-C a en fait try/catch/finalement/throw, mais vous les utilisez rarement). Swift vient de supprimer la possibilité d'intercepter des exceptions.

Swift a une fonctionnalité qui ressemble à la gestion des exceptions mais est juste une gestion des erreurs appliquée. Historiquement, Objective-C avait un modèle de gestion des erreurs assez omniprésent: une méthode renvoyait soit un BOOL (OUI pour le succès) ou une référence d'objet (nul pour l'échec, pas nul pour le 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, Swift peuvent facilement renvoyer des alternatives, comme un résultat si une fonction a bien fonctionné et une erreur si elle a échoué; cela facilite beaucoup la gestion des erreurs. Mais la réponse à la question d'origine: Les concepteurs de Swift pensaient évidemment que créer un langage sûr et écrire du code réussi dans un tel langage est plus facile si le langage n'a pas d'exceptions.

3
gnasher729

En plus de la réponse de Charlie:

Ces exemples de gestion d'exceptions déclarées que vous voyez dans de nombreux manuels et livres ne semblent très intelligents que sur de très petits exemples.

Même si vous mettez de côté l'argument sur l'état d'objet invalide, ils causent toujours une énorme douleur lors du traitement d'une grande application.

Par exemple, lorsque vous devez gérer des E/S, en utilisant de la cryptographie, vous pouvez avoir 20 types d'exceptions qui peuvent être levé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 quand une exception ne peut pas apparaître et vous n'avez tout simplement jamais besoin d'écrire autant de gestion des exceptions, vous utilisez donc quelques solutions pour ignorer les exceptions déclarées. Dans ma pratique, seulement environ 5% des exceptions déclarées doivent être traitées dans le code pour avoir une application fiable.

2
konmik
 int result;
 if((result = operation_that_can_throw_ioerror()) == IOError)
 {
  handle_the_exception_somehow();
 }
 else
 {
   # we don't want to catch the IOError if it's raised
   result = another_operation_that_can_throw_ioerror();
 }
 result |= something_we_always_need_to_do();
 return result;

En C, vous vous retrouveriez avec quelque chose comme ce qui précède.

Y a-t-il des choses que je ne peux pas faire dans Swift que je pourrais faire avec des exceptions?

Non, il n'y a rien. Vous finissez par gérer des codes de résultat 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 heureux, mais c'est à peu près tout.

2
stonemetal