Exceptions, codes d'erreur et syndicats discriminés


80

J'ai récemment commencé un travail de programmation en C #, mais j'ai pas mal de connaissances en Haskell.

Mais je comprends que C # est un langage orienté objet, je ne veux pas forcer une cheville ronde dans un trou carré.

J'ai lu l'article Exception Throwing de Microsoft qui dit:

NE PAS renvoyer les codes d'erreur.

Mais étant habitué à Haskell, j'utilise le type de données C # OneOf, renvoyant le résultat sous forme de valeur "droite" ou l'erreur (le plus souvent une énumération) sous forme de valeur "à gauche".

Cela ressemble beaucoup à la convention de EitherHaskell.

Pour moi, cela semble plus sûr que les exceptions. En C #, ignorer les exceptions ne génère pas d'erreur de compilation et si elles ne sont pas interceptées, elles ne font que bouillonner et faire planter votre programme. C'est peut-être mieux que d'ignorer un code d'erreur et de produire un comportement indéfini, mais bloquer le logiciel de votre client n'est toujours pas une bonne chose, en particulier lorsqu'il effectue de nombreuses autres tâches importantes en arrière-plan.

Avec OneOf, il faut être assez explicite sur le déballage et le traitement de la valeur de retour et des codes d'erreur. Et si on ne sait pas comment le gérer à ce stade de la pile d'appels, il doit être placé dans la valeur de retour de la fonction actuelle, afin que les appelants sachent qu'une erreur peut en résulter.

Mais cela ne semble pas être l'approche suggérée par Microsoft.

L’utilisation OneOfd’exceptions au lieu d’exceptions pour la gestion des exceptions "ordinaires" (comme File Not Found, etc.) est-elle une approche raisonnable ou est-ce une pratique épouvantable?


Il est à noter que j'ai entendu dire que les exceptions en tant que flux de contrôle sont considérées comme un anti-modèle sérieux , donc si "l'exception" est quelque chose que vous géreriez normalement sans mettre fin au programme, n'est-ce pas un "flux de contrôle"? Je comprends qu'il y a une zone d'ombre.

Remarque Je n'utilise pas OneOfpour des choses comme "Out Of Memory", des conditions dont je ne m'attends pas à récupérer vont toujours générer des exceptions. Mais je me sens comme des problèmes tout à fait raisonnables, par exemple, une entrée utilisateur qui n’analyse pas est essentiellement un "flux de contrôle" et ne devrait probablement pas renvoyer d’exception.


Pensées suivantes:

Ce que je retiens actuellement de cette discussion est le suivant:

  1. Si vous vous attendez à ce que l'appelant immédiat catchtraite et gère l'exception la plupart du temps et poursuive son travail, peut-être par un autre chemin, elle devrait probablement faire partie du type de retour. Optionalou OneOfpeut être utile ici.
  2. Si vous vous attendez à ce que l'appelant immédiat ne détecte pas l'exception la plupart du temps, lancez une exception pour éviter la stupidité de la transmettre manuellement dans la pile.
  3. Si vous n'êtes pas sûr de ce que l'appelant immédiat va faire, fournissez peut-être les deux, comme Parseet TryParse.

13
Utilisez F # Result;-)
Astrinus

8
Un peu hors sujet, mais notez que l'approche que vous envisagez présente la vulnérabilité générale suivante: il est impossible de garantir que le développeur gère correctement l'erreur. Bien sûr, le système de dactylographie peut servir de rappel, mais vous perdez le défaut de faire exploser le programme pour ne pas avoir géré une erreur, ce qui augmente le risque que vous vous retrouviez dans un état inattendu. Que le rappel vaille la peine de perdre ce défaut est un débat ouvert, cependant. J'ai tendance à penser qu'il est préférable d'écrire tout votre code en supposant qu'une exception le finira à un moment donné; alors le rappel a moins de valeur.
jpmc26

9
Article connexe: Eric Lippert sur les quatre "seaux" d'exceptions . L'exemple qu'il donne sur les exceptions exogènes montre qu'il existe des scénarios dans lesquels il suffit de gérer des exceptions, par exemple FileNotFoundException.
Søren D. Ptæus

7
Je peux être seul à ce sujet , mais je pense que plantage est bon . Il n’existe pas de meilleure preuve que votre logiciel pose problème, c’est un journal des collisions avec le suivi approprié. Si votre équipe est capable de corriger et de déployer un correctif en 30 minutes ou moins, laisser ces vilains copains se présenter peut ne pas être une mauvaise chose.
T. Sar - Réintégrer Monica

9
Comment se fait-il qu'aucune des réponses ne précise que la Eithermonade n'est pas un code d'erreur et qu'elle ne l'est pas non plus OneOf? Elles sont fondamentalement différentes et la question semble donc reposer sur un malentendu. (Bien que sous une forme modifiée, c'est toujours une question valable.)
Konrad Rudolph

Réponses:


131

Mais planter le logiciel de votre client n'est toujours pas une bonne chose

C'est très certainement une bonne chose.

Vous voulez que tout ce qui laisse le système dans un état non défini arrête le système car un système non défini peut faire des choses désagréables comme des données corrompues, formater le disque dur et envoyer des courriers électroniques menaçants au président. Si vous ne pouvez pas restaurer le système et le remettre dans un état défini, il est responsable de la panne. C'est exactement pourquoi nous construisons des systèmes qui plantent plutôt que de se déchirer tranquillement. Bien sûr, nous voulons tous un système stable qui ne se bloque jamais, mais nous ne le souhaitons vraiment que lorsque le système reste dans un état de sécurité prévisible et défini.

J'ai entendu dire que les exceptions en tant que flux de contrôle sont considérées comme un anti-modèle sérieux

C'est absolument vrai, mais c'est souvent mal compris. Lorsqu'ils ont inventé le système d'exception, ils craignaient de rompre la programmation structurée. La programmation structurée est pourquoi nous avons for, while, until, breaket continuequand tout ce que nous devons, de faire tout cela, est goto.

Dijkstra nous a appris que l'utilisation de goto de manière informelle (c'est-à-dire que vous pouvez sauter où bon vous semble) fait de la lecture de code un cauchemar. Quand ils nous ont donné le système d'exception, ils ont eu peur de réinventer la méthode goto. Alors ils nous ont dit de ne pas "l'utiliser pour le contrôle de flux" en espérant que nous comprendrions. Malheureusement, beaucoup d'entre nous n'ont pas.

Bizarrement, nous n'utilisons pas souvent les exceptions pour créer du code spaghetti comme nous le faisions auparavant avec goto. Le conseil lui-même semble avoir causé plus de problèmes.

Fondamentalement, les exceptions consistent à rejeter une hypothèse. Lorsque vous demandez la sauvegarde d’un fichier, vous supposez qu’il peut et sera sauvegardé. L'exception que vous obtenez quand il ne peut pas être peut-être sur le nom étant illégal, le HD étant plein, ou parce qu'un rat a rongé à travers votre câble de données. Vous pouvez gérer toutes ces erreurs différemment, de la même manière ou les laisser arrêter le système. Il y a un chemin heureux dans votre code où vos hypothèses doivent être vérifiées. D'une manière ou d'une autre, des exceptions vous éloignent de cette voie heureuse. Strictement parlant, oui, c'est une sorte de "contrôle de flux", mais ce n'est pas pour cela qu'ils vous ont mis en garde. Ils parlaient de bêtises comme ceci :

entrez la description de l'image ici

"Les exceptions devraient être exceptionnelles". Cette petite tautologie est née car les concepteurs de systèmes d'exception ont besoin de temps pour créer des traces de pile. Comparé aux sauts, c'est lent. Il mange du temps de calcul. Mais si vous êtes sur le point de vous connecter et d’arrêter le système ou du moins d’interrompre le traitement qui prend beaucoup de temps avant de commencer le suivant, vous avez un peu de temps à perdre. Si les gens commencent à utiliser des exceptions "pour le contrôle de flux", ces hypothèses sur le temps disparaissent toutes. Donc, "Les exceptions devraient être exceptionnelles" nous a vraiment été donné comme une considération de performance.

Bien plus important que cela ne nous confond pas. Combien de temps vous a-t-il fallu pour repérer la boucle infinie dans le code ci-dessus?

NE PAS renvoyer les codes d'erreur.

... est un bon conseil lorsque vous vous trouvez dans une base de code qui n'utilise généralement pas de codes d'erreur. Pourquoi? Parce que personne ne se souviendra de sauvegarder la valeur de retour et de vérifier vos codes d'erreur. C'est toujours une bonne convention quand on est en C.

OneOf

Vous utilisez encore une autre convention. C'est bien tant que vous établissez la convention et que vous ne vous contentez pas d'en combattre une autre. Il est déroutant d'avoir deux conventions d'erreur dans la même base de code. Si, d'une manière ou d'une autre, vous vous êtes débarrassé de tout le code qui utilise l'autre convention, continuez.

J'aime la convention moi-même. Une des meilleures explications que j'ai trouvées ici * :

entrez la description de l'image ici

Mais même si cela me plaît, je ne vais pas encore le mélanger avec les autres conventions. Choisissez en un et gardez le. 1

1: Je veux dire par là ne me fait pas penser à plus d’une convention à la fois.


Pensées suivantes:

Ce que je retiens actuellement de cette discussion est le suivant:

  1. Si vous vous attendez à ce que l'appelant immédiat attrape et traite l'exception la plupart du temps et poursuive son travail, peut-être par un autre chemin, elle devrait probablement faire partie du type de retour. Optionnel ou OneOf peut être utile ici.
  2. Si vous vous attendez à ce que l'appelant immédiat ne détecte pas l'exception la plupart du temps, lancez une exception pour éviter la stupidité de la transmettre manuellement dans la pile.
  3. Si vous ne savez pas ce que l'appelant immédiat va faire, fournissez peut-être les deux, comme Parse et TryParse.

Ce n'est vraiment pas si simple. Une des choses fondamentales que vous devez comprendre est ce qu'est un zéro.

Combien de jours reste-t-il en mai? 0 (parce que ce n'est pas mai, c'est déjà juin).

Les exceptions sont un moyen de rejeter une hypothèse, mais ce n'est pas le seul moyen. Si vous utilisez des exceptions pour rejeter l’hypothèse, vous quittez la bonne voie. Mais si vous choisissez des valeurs pour indiquer le chemin heureux qui signale que les choses ne sont pas aussi simples qu'on le supposait, vous pouvez rester sur ce chemin tant qu'il peut gérer ces valeurs. Parfois, 0 est déjà utilisé pour signifier quelque chose, vous devez donc trouver une autre valeur pour cartographier votre hypothèse de rejet de l'idée. Vous pouvez reconnaître cette idée de son utilisation dans la bonne vieille algèbre . Les monades peuvent aider, mais il ne faut pas toujours que ce soit une monade.

Par exemple 2 :

IList<int> ParseAllTheInts(String s) { ... }

Pouvez-vous penser à une bonne raison pour que cela soit conçu de manière à ce qu'il jette délibérément quoi que ce soit? Devinez ce que vous obtenez quand aucun int ne peut être analysé? Je n'ai même pas besoin de te dire.

C'est un signe d'un bon nom. Désolé, TryParse n'est pas mon idée d'un bon nom.

Nous évitons souvent de faire une exception pour ne rien obtenir alors que la réponse pourrait être plus d'une chose à la fois, mais pour une raison quelconque, si la réponse est une chose ou rien, nous sommes obsédés d'insister pour qu'elle nous donne une chose ou un jet:

IList<Point> Intersection(Line a, Line b) { ... }

Les lignes parallèles doivent-elles vraiment causer une exception ici? Est-ce vraiment si grave que cette liste ne contienne jamais plus d'un point?

Peut-être que, sémantiquement, vous ne pouvez pas supporter ça. Si c'est le cas, c'est dommage. Mais peut-être que les monades, qui n'ont pas une taille arbitraire List, vous feront vous sentir mieux.

Maybe<Point> Intersection(Line a, Line b) { ... }

Les Monads sont de petites collections à usage spécial conçues pour être utilisées de manière spécifique, sans avoir à les tester. Nous sommes censés trouver des moyens de les gérer, peu importe ce qu’ils contiennent. De cette façon, le chemin heureux reste simple. Si vous ouvrez et testez chaque Monad que vous touchez, vous l'utiliserez mal.

Je sais, c'est bizarre. Mais c'est un nouvel outil (enfin, pour nous). Alors donnez-lui du temps. Les marteaux ont plus de sens lorsque vous cessez de les utiliser avec des vis.


Si vous me le permettez, j'aimerais répondre à ce commentaire:

Comment se fait-il qu'aucune des réponses ne précise que l'une des deux monades n'est pas un code d'erreur, et OneOf non plus? Elles sont fondamentalement différentes et la question semble donc reposer sur un malentendu. (Bien que sous une forme modifiée, cela reste une question valable.) - Konrad Rudolph le 4 juin à 14:08

C'est absolument vrai. Les monades sont beaucoup plus proches des collections que les exceptions, les drapeaux ou les codes d'erreur. Ils fabriquent de beaux récipients pour de telles choses quand ils sont utilisés judicieusement.


9
Ne serait-il pas plus approprié que la piste rouge soit au- dessous des cases et ne les traverse donc pas?
Flater

4
Logiquement, vous avez raison. Toutefois, chaque zone de ce diagramme représente une opération de liaison monadique. bind inclut (peut-être crée!) la piste rouge. Je recommande de regarder la présentation complète. C'est intéressant et Scott est un excellent orateur.
Gusdor

7
Je pense que les exceptions ressemblent plus à une instruction COMEFROM qu'à un GOTO, car throwje ne sais pas vraiment ou ne pas dire où nous allons sauter;)
Warbo

3
Il n’ya rien de mal à ce que les conventions de mélange soient établies si vous pouvez établir des limites, c’est en quoi consiste l’encapsulation.
Robert Harvey

4
Toute la première partie de cette réponse se lit comme si vous aviez arrêté de lire la question après la phrase citée. La question reconnaît qu'il est préférable de planter le programme que de passer à un comportement indéfini: ils contrastent les collisions au moment de l'exécution, pas avec l'exécution continue indéfinie, mais avec les erreurs de compilation qui vous empêchent même de construire le programme sans prendre en compte l'erreur potentielle. et le faire (ce qui peut devenir un crash s'il n'y a rien d'autre à faire, mais ce sera un crash parce que vous le souhaitez, pas parce que vous l'avez oublié).
Kryan

35

C # n'est pas Haskell et vous devriez suivre le consensus des experts de la communauté C #. Si vous essayez plutôt de suivre les pratiques Haskell dans un projet C #, vous aliénerez tous les autres membres de votre équipe et vous découvrirez probablement les raisons pour lesquelles la communauté C # fait les choses différemment. Une des principales raisons est que C # ne soutient pas convenablement les syndicats discriminés.

J'ai entendu dire que les exceptions en tant que flux de contrôle sont considérées comme un anti-modèle sérieux,

Ce n'est pas une vérité universellement acceptée. Le choix dans chaque langue qui prend en charge les exceptions est soit de lancer une exception (que l'appelant est libre de ne pas gérer), soit de retourner une valeur composée (que l'appelant DOIT gérer).

La propagation des conditions d'erreur à travers la pile d'appels requiert une condition à chaque niveau, ce qui double la complexité cyclomatique de ces méthodes et par conséquent le nombre de tests unitaires. Dans les applications métier classiques, de nombreuses exceptions vont au-delà de la récupération et peuvent se propager au niveau le plus élevé (par exemple, le point d'entrée de service d'une application Web).


9
La propagation des monades ne nécessite rien.
Basilevs

23
@Basilevs Il nécessite un support pour propager des monades tout en maintenant le code lisible, ce que C # n'a pas. Vous obtenez soit des instructions répétées si-retour ou des chaînes de lambdas.
Sebastian Redl

4
@SebastianRedl lambdas semblent OK en C #.
Basilevs

@Basilevs D'un point de vue syntaxique, oui, mais du point de vue des performances, je suppose qu'ils pourraient être moins attrayants.
Pharap

5
En C # moderne, vous pouvez probablement utiliser partiellement les fonctions locales pour récupérer un peu de vitesse. Dans tous les cas, la plupart des logiciels de gestion sont considérablement ralentis par IO.
Turksarama

26

Vous êtes arrivé à C # à un moment intéressant. Jusqu'à récemment, le langage était fermement ancré dans l'espace de programmation impératif. L'utilisation d'exceptions pour communiquer des erreurs était définitivement la norme. L'utilisation de codes de retour souffrait d'un manque de prise en charge linguistique (par exemple autour des unions discriminées et des correspondances de modèle). Assez raisonnablement, les directives officielles de Microsoft consistaient à éviter les codes de retour et à utiliser des exceptions.

Il y a toujours eu des exceptions à cela, toutefois, sous la forme de TryXXXméthodes, qui renvoient un booleanrésultat de succès et fournissent un résultat de seconde valeur via un outparamètre. Celles-ci sont très similaires aux modèles "try" de la programmation fonctionnelle, sauf que le résultat provient d'un paramètre out plutôt que d'une Maybe<T>valeur de retour.

Mais les choses changent. La programmation fonctionnelle devient de plus en plus populaire et des langages tels que C # répondent. C # 7.0 a introduit certaines fonctionnalités de correspondance de modèle de base dans le langage; C # 8 en introduira beaucoup plus, y compris une expression de commutateur, des modèles récursifs, etc. Parallèlement, les "bibliothèques fonctionnelles" pour C # se sont multipliées, telles que ma propre bibliothèque Succinc <T> , qui prend également en charge les syndicats discriminés. .

Ces deux éléments combinés signifient que la popularité du code comme celui-ci augmente lentement.

static Maybe<int> TryParseInt(string source) =>
    int.TryParse(source, out var result) ? new Some<int>(result) : none; 

public int GetNumberFromUser()
{
    Console.WriteLine("Please enter a number");
    while (true)
    {
        var userInput = Console.ReadLine();
        if (TryParseInt(userInput) is int value)
        {
            return value;
        }
        Console.WriteLine("That's not a valid number. Please try again");
    }
}

C'est encore assez une niche pour le moment, même si au cours des deux dernières années, j'ai constaté un net changement de l'hostilité générale des développeurs C # vers ce type de code et un intérêt croissant pour l'utilisation de telles techniques.

Nous ne sommes pas encore là pour dire " NE retournez PAS les codes d'erreur". est désuet et doit prendre sa retraite. Mais la marche vers la réalisation de cet objectif est bien engagée. Alors, faites votre choix: restez fidèles aux anciennes méthodes de rejet des exceptions avec abandon gay si vous aimez faire les choses comme vous le souhaitez. ou commencez à explorer un nouveau monde où les conventions des langages fonctionnels sont de plus en plus populaires en C #.


22
C'est pourquoi je déteste le C # :) Ils continuent à ajouter des méthodes pour écorcher un chat et, bien sûr, les anciennes méthodes ne disparaissent jamais. En fin de compte, il n'y a pas de convention pour rien, un programmeur perfectionniste peut rester assis pendant des heures à décider quelle méthode est la plus «agréable» et un nouveau programmeur doit tout apprendre avant de pouvoir lire le code des autres.
Aleksandr Dubinsky

24
@ AleksandrDubinsky, vous avez trouvé un problème fondamental avec les langages de programmation en général, pas seulement en C #. De nouvelles langues sont créées, minces, fraîches et pleines d’idées modernes. Et très peu de gens les utilisent. Au fil du temps, ils obtiennent des utilisateurs et de nouvelles fonctionnalités, en plus d'anciennes fonctionnalités qui ne peuvent pas être supprimées car cela casserait le code existant. Le ballot grossit. Alors, les gens créent de nouvelles langues maigres, fraîches et pleines d’idées modernes ...
David Arno

7
@AleksandrDubinsky ne vous détestez pas quand ils ajoutent maintenant des fonctionnalités des années 1960.
ctrl-alt-delor

6
@ AleksandrDubinsky Ouais. Parce que Java a essayé d'ajouter une chose (les génériques) une fois , ils ont échoué, nous devons maintenant vivre avec le désordre horrible qui en a résulté. Ils ont donc appris la leçon et arrêté d'ajouter quoi que ce soit
:)

3
@Agent_L: Vous savez que Java est ajouté java.util.Stream(un IEnumerable-semblable à moitié assod) et lambdas maintenant, non? :)
cHao

5

Je pense qu'il est parfaitement raisonnable d'utiliser un DU pour renvoyer des erreurs, mais je dirais alors que j'ai écrit OneOf :) Mais je dirais quand même, car c'est une pratique courante en F #, etc. ).

Je ne pense pas que les erreurs que je retourne en général sont des situations exceptionnelles, mais plutôt normales, mais j'espère que rarement rencontrées des situations qui peuvent, ou ne doivent pas nécessairement être traitées, mais doivent être modélisées.

Voici l'exemple de la page du projet.

public OneOf<User, InvalidName, NameTaken> CreateUser(string username)
{
    if (!IsValid(username)) return new InvalidName();
    var user = _repo.FindByUsername(username);
    if(user != null) return new NameTaken();
    var user = new User(username);
    _repo.Save(user);
    return user;
}

Lancer des exceptions pour ces erreurs semble exagéré - elles ont une surcharge de performances, outre les inconvénients de ne pas être explicite dans la signature de la méthode, ou de fournir une correspondance exhaustive.

Dans quelle mesure OneOf est idiomatique en c #, c'est une autre question. La syntaxe est valide et raisonnablement intuitive. Les DU et la programmation orientée rail sont des concepts bien connus.

Je voudrais sauver des exceptions pour des choses qui indiquent que quelque chose est cassé lorsque cela est possible.


Ce que vous dites, c’est qu’il OneOfs’agit de rendre la valeur de retour plus riche, comme de retourner un Nullable. Il remplace les exceptions pour les situations qui ne sont pas exceptionnelles pour commencer.
Aleksandr Dubinsky

Oui - et fournit un moyen facile de faire une correspondance exhaustive dans l'appelant, ce qui n'est pas possible avec une hiérarchie des résultats basée sur l'héritage.
mcintyre321

4

OneOfest à peu près équivalent aux exceptions vérifiées en Java. Il y a quelques différences:

  • OneOfne fonctionne pas avec les méthodes produisant des méthodes appelées pour leurs effets secondaires (car elles peuvent être appelées de manière significative et le résultat simplement ignoré). Évidemment, nous devrions tous essayer d'utiliser des fonctions pures, mais ce n'est pas toujours possible.

  • OneOfne fournit aucune information sur l'endroit où le problème s'est produit. Cela est utile car cela permet d'économiser les performances (les exceptions émises en Java sont coûteuses en raison du remplissage de la trace de pile et en C #, ce sera la même chose) et vous oblige à fournir suffisamment d'informations dans le membre d'erreur de OneOflui - même. C'est également mauvais, car ces informations peuvent être insuffisantes et vous pouvez avoir du mal à trouver l'origine du problème.

OneOf et exception cochée ont une "fonctionnalité" importante en commun:

  • ils vous obligent tous les deux à les gérer partout dans la pile d'appels

Cela empêche votre peur

En C #, ignorer les exceptions ne génère pas d'erreur de compilation et si elles ne sont pas interceptées, elles ne font que bouillonner et faire planter votre programme.

mais comme déjà dit, cette peur est irrationnelle. En gros, il y a généralement une seule place dans votre programme, où vous devez tout attraper (il est probable que vous utilisiez déjà un framework pour le faire). Comme vous et votre équipe passez généralement des semaines ou des années à travailler sur le programme, vous ne l'oublierez pas.

Le gros avantage des exceptions ignorables est qu'elles peuvent être gérées n'importe où, plutôt que partout dans la trace de pile. Cela élimine des tonnes de passe-partout (il suffit de regarder du code Java déclarant des "lancers ...." ou des exceptions d'encapsulation) et cela rend également votre code moins bogué: Comme toute méthode peut lancer, vous devez en être conscient. Heureusement, la bonne chose à faire est généralement de ne rien faire , c'est-à-dire de la laisser bouillonner jusqu'à un endroit où elle peut être traitée de manière raisonnable.


+1 mais pourquoi OneOf ne fonctionne-t-il pas avec des effets secondaires? (J'imagine que c'est parce qu'il passera un chèque si vous n'attribuez pas la valeur de retour, mais comme je ne connais pas OneOf, ni beaucoup de C #, je ne suis pas sûr - une clarification améliorerait votre réponse.)
dcorking

1
Vous pouvez renvoyer des informations d'erreur avec OneOf en faisant en sorte que l'objet d'erreur renvoyé contienne des informations utiles, par exemple, OneOf<SomeSuccess, SomeErrorInfo>SomeErrorInfofournit les détails de l'erreur.
mcintyre321

@dcorking Edité. Bien sûr, si vous avez ignoré la valeur de retour, vous ne savez rien du problème. Si vous faites cela avec une fonction pure, c'est bon (vous venez de perdre du temps).
Maaartinus

2
@ mcintyre321 Bien sûr, vous pouvez ajouter des informations, mais vous devez le faire manuellement et c'est sujet à la paresse et à l'oubli. Je suppose que, dans les cas plus complexes, une trace de pile est plus utile.
Maaartinus

4

L’utilisation de OneOf au lieu d’exceptions pour la gestion des exceptions "ordinaires" (comme File Not Found, etc.) est-elle une approche raisonnable ou est-ce une pratique terrible?

Il est à noter que j'ai entendu dire que les exceptions en tant que flux de contrôle sont considérées comme un anti-modèle sérieux, donc si "l'exception" est quelque chose que vous géreriez normalement sans mettre fin au programme, n'est-ce pas un "flux de contrôle"?

Les erreurs ont un certain nombre de dimensions. J'identifie trois dimensions: peuvent-elles être empêchées, à quelle fréquence, et si elles peuvent être récupérées. Pendant ce temps, la signalisation d'erreur a principalement une dimension: décider dans quelle mesure battre l'appelant au-dessus de la tête pour le forcer à gérer l'erreur ou laisser l'erreur "discrètement" s'afficher comme une exception.

Les erreurs non évitables, fréquentes et récupérables sont celles qui doivent vraiment être traitées sur le site de l'appel. Ils justifient le mieux de forcer l'appelant à les confronter. En Java, je leur fais des exceptions vérifiées, et un effet similaire est obtenu en leur rendant des Try*méthodes ou en renvoyant une union discriminée. Les unions discriminées sont particulièrement utiles lorsqu'une fonction a une valeur de retour. Ils sont une version beaucoup plus raffinée du retour null. La syntaxe des try/catchblocs n'est pas bonne (trop de crochets), ce qui rend les alternatives plus belles. De plus, les exceptions sont légèrement lentes car elles enregistrent des traces de pile.

Les erreurs évitables (qui n'ont pas été évitées en raison d'une erreur ou d'une négligence du programmeur) et les erreurs non récupérables fonctionnent vraiment bien comme exceptions ordinaires. Les erreurs peu fréquentes fonctionnent également mieux en tant qu'exceptions ordinaires, puisqu'un programmeur peut souvent penser que cela ne vaut pas la peine d'anticiper (cela dépend de l'objectif du programme).

Un point important est que c’est souvent le site d’utilisation qui détermine la manière dont les modes d’erreur de la méthode s’adaptent à ces dimensions, ce qui doit être pris en compte lors de la prise en compte des éléments suivants.

D'après mon expérience, forcer l'appelant à faire face aux conditions d'erreur est acceptable lorsque les erreurs sont évitables, fréquentes et récupérables, mais cela devient très agaçant lorsque l'appelant est obligé de sauter à travers des cerceaux lorsqu'il n'en a pas besoin ou ne le souhaite pas . Cela peut être dû au fait que l'appelant sait que l'erreur ne se produira pas ou parce qu'il ne veut pas (encore) rendre le code résistant. En Java, cela explique l'angoisse suscitée par le recours fréquent aux exceptions vérifiées et au renvoi de Optional (similaire à Maybe et qui a récemment été introduit malgré de nombreuses controverses). En cas de doute, lancez des exceptions et laissez l'appelant décider de la façon dont il souhaite les gérer.

Enfin, gardez à l'esprit que la chose la plus importante concernant les conditions d'erreur, bien au-delà de toute autre considération telle que la manière dont elles sont signalées, est de les documenter de manière approfondie .


2

Les autres réponses ont traité des exceptions par rapport aux codes d'erreur de manière suffisamment détaillée. J'aimerais donc ajouter une autre perspective spécifique à la question:

OneOf n'est pas un code d'erreur, c'est plutôt une monade

La manière dont il est utilisé OneOf<Value, Error1, Error2>est un conteneur qui représente le résultat réel ou un état d'erreur. C'est comme un Optional, sauf que lorsqu'aucune valeur n'est présente, cela peut fournir plus de détails.

Le principal problème avec les codes d'erreur est que vous oubliez de les vérifier. Mais ici, vous ne pouvez littéralement pas accéder au résultat sans vérifier les erreurs.

Le seul problème est que vous ne vous souciez pas du résultat. L'exemple de la documentation de OneOf donne:

public OneOf<User, InvalidName, NameTaken> CreateUser(string username) { ... }

... puis ils créent des réponses HTTP différentes pour chaque résultat, ce qui est bien mieux que d'utiliser des exceptions. Mais si vous appelez la méthode comme ça.

public void CreateUserButtonClick(String username) {
    UserManager.CreateUser(string username)
}

alors vous n'avez un problème et exceptions serait le meilleur choix. Comparer Rail vs .savesave!


1

Une chose à considérer est que le code en C # n'est généralement pas pur. Dans une base de code pleine d'effets secondaires et avec un compilateur qui se fiche de ce que vous faites avec la valeur de retour d'une fonction, votre approche peut vous causer beaucoup de chagrin. Par exemple, si vous avez une méthode qui supprime un fichier, vous voulez vous assurer que l'application avertit quand la méthode a échoué, même s'il n'y a aucune raison de vérifier le code d'erreur "sur le chemin heureux". C'est une différence considérable par rapport aux langages fonctionnels qui vous obligent à ignorer explicitement les valeurs de retour que vous n'utilisez pas. En C #, les valeurs de retour sont toujours implicitement ignorées (la seule exception étant les getters de propriétés IIRC).

La raison pour laquelle MSDN vous dit de "ne pas renvoyer de codes d'erreur" est exactement la même: personne ne vous oblige même à lire le code d'erreur. Le compilateur ne vous aide pas du tout. Cependant, si votre fonction est exempte d’effets secondaires, vous pouvez utiliser en toute sécurité quelque chose comme Either: le fait est que même si vous ignorez le résultat de l’erreur (le cas échéant), vous ne pouvez le faire que si vous n’utilisez pas le "résultat correct" Soit. Il y a quelques façons comment vous pouvez y parvenir - par exemple, vous pouvez autoriser uniquement les « lire » la valeur en passant un délégué pour gérer le succès et les cas d'erreur, et vous pouvez facilement combiner avec des approches telles que la programmation orientée des chemins de fer (il ne fonctionne très bien en C #).

int.TryParseest quelque part au milieu. C'est pur Il définit les valeurs des deux résultats à tout moment. Vous savez donc que si la valeur renvoyée est false, le paramètre de sortie sera défini sur 0. Cela ne vous empêche toujours pas d'utiliser le paramètre de sortie sans vérifier la valeur de retour, mais au moins, vous êtes assuré du résultat, même si la fonction échoue.

Mais une chose absolument cruciale ici est la cohérence . Le compilateur ne va pas vous sauver si quelqu'un change cette fonction pour avoir des effets secondaires. Ou jeter des exceptions. Ainsi, bien que cette approche fonctionne parfaitement en C # (et que je l'utilise bien), vous devez vous assurer que tous les membres de votre équipe la comprennent et l'utilise . La plupart des invariants nécessaires au bon fonctionnement de la programmation fonctionnelle ne sont pas appliqués par le compilateur C # et vous devez vous assurer qu'ils sont suivis par vous-même. Si vous ne respectez pas les règles que Haskell applique par défaut, vous devez vous tenir au courant de ce qui ne fonctionne pas. Vous devez en tenir compte pour tous les paradigmes fonctionnels que vous introduisez dans votre code.


0

La déclaration correcte serait Ne pas utiliser les codes d'erreur pour les exceptions! Et n'utilisez pas d'exceptions pour des non-exceptions.

Si quelque chose arrive que votre code ne peut pas récupérer (c'est-à-dire le faire fonctionner), c'est une exception. Votre code immédiat ne ferait rien avec un code d'erreur, si ce n'est de le remonter pour le connecter ou peut-être de fournir le code à l'utilisateur d'une manière ou d'une autre. Cependant, c’est précisément à cela que servent les exceptions - elles traitent de tous les problèmes et aident à analyser ce qui s’est réellement passé.

Cependant, si quelque chose se produit, telle qu'une entrée invalide que vous pouvez gérer, soit en la reformatant automatiquement, soit en demandant à l'utilisateur de corriger les données (et vous y avez pensé), ce n'est pas vraiment une exception, car attendez-le. Vous pouvez vous sentir libre de gérer ces cas avec des codes d'erreur ou toute autre architecture. Juste pas des exceptions.

(Notez que je ne suis pas très expérimenté en C #, mais ma réponse devrait être valable dans le cas général en fonction du niveau conceptuel des exceptions.)


7
Je suis fondamentalement en désaccord avec la prémisse de cette réponse. Si les concepteurs de langage ne catchsouhaitaient pas que des exceptions soient utilisées pour les erreurs récupérables, le mot clé n'existerait pas. Et comme nous parlons dans un contexte C #, il convient de noter que la philosophie adoptée ici n’est pas suivie par le framework .NET; L’exemple le plus simple et imaginable de «saisie invalide que vous puissiez gérer» est peut- être l’utilisateur qui saisit un non-nombre dans un champ où un nombre est attendu ... et int.Parselève une exception.
Mark Amery

@MarkAmery Pour int.Parse, rien ne peut être récupéré, ni rien d’attendu. La clause catch est généralement utilisée pour éviter l'échec complet d'une vue extérieure, elle ne demandera pas à l'utilisateur de nouvelles informations d'identification de base de données ou autres, cela évitera le blocage du programme. C'est une défaillance technique et non un flux de contrôle d'application.
Frank Hopkins

3
La question de savoir s'il est préférable qu'une fonction lève une exception lorsqu'une condition survient dépend de la capacité de l' appelant immédiat de la fonction à gérer cette condition. Dans les cas où cela est possible, le lancement d'une exception que l'appelant immédiat doit intercepter représente un travail supplémentaire pour l'auteur du code appelant et un travail supplémentaire pour la machine qui le traite. Si un appelant n'est pas prêt à gérer une condition, toutefois, des exceptions évitent à l'appelant de le coder explicitement.
Supercat

@Darkwing "Vous pouvez vous sentir libre de gérer ces cas avec des codes d'erreur ou toute autre architecture". Oui, vous êtes libre de le faire. Cependant, vous ne recevez aucune aide de .NET pour ce faire, puisque le CL .NET lui-même utilise des exceptions au lieu de codes d'erreur. Ainsi, dans de nombreux cas, vous êtes obligé de capturer les exceptions .NET et de renvoyer le code d'erreur correspondant au lieu de laisser les exceptions bouillonner, vous forcez également les membres de votre équipe à utiliser un autre concept de gestion des erreurs qu'ils doivent utiliser parallèlement aux exceptions, le concept de traitement des erreurs standard.
Aleksander

-2

C'est une "Terrible Idea".

C'est terrible parce que, du moins dans .net, vous ne disposez pas d'une liste exhaustive des exceptions possibles. N'importe quel code peut éventuellement renvoyer une exception. Surtout si vous faites de la programmation orientée objet et pouvez appeler des méthodes substituées sur un sous-type plutôt que sur le type déclaré

Vous devrez donc modifier toutes les méthodes et fonctions OneOf<Exception, ReturnType>, puis, si vous souhaitez gérer différents types d'exceptions, vous devrez examiner le type, If(exception is FileNotFoundException)etc.

try catch est l'équivalent de OneOf<Exception, ReturnType>

Modifier ----

Je suis conscient que vous proposez de revenir, OneOf<ErrorEnum, ReturnType>mais j'estime qu'il s'agit d'une distinction sans différence.

Vous combinez les codes de retour, les exceptions attendues et les branches par exception. Mais l'effet global est le même.


2
Les exceptions «normales» sont également une idée terrible. l'indice est dans le nom
Ewan

4
Je ne propose pas de rendre l' Exceptionart.
Clinton

proposez-vous que l'alternative à l'un des contrôles soit le flux de contrôle via des exceptions
Ewan
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.