J'ai récemment commencé un travail de programmation C #, mais j'ai un peu d'expérience dans 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 des codes d'erreur.
Mais étant habitué à Haskell, j'ai utilisé le type de données C # OneOf
, renvoyant le résultat comme la "bonne" valeur ou l'erreur (le plus souvent une énumération) comme " gauche "valeur.
Cela ressemble beaucoup à la convention Either
dans Haskell.
Pour moi, cela semble plus sûr que les exceptions. En C #, ignorer les exceptions ne produit pas d'erreur de compilation, et si elles ne sont pas interceptées, elles bouillonnent et plantent votre programme. C'est peut-être mieux que d'ignorer un code d'erreur et de produire un comportement indéfini, mais planter le logiciel de votre client n'est toujours pas une bonne chose, en particulier lorsqu'il exécute de nombreuses autres tâches commerciales importantes en arrière-plan.
Avec OneOf
, il faut être assez explicite pour le déballer et gérer la valeur de retour et les codes d'erreur. Et si l'on ne sait pas comment le gérer à ce stade de la pile d'appels, il doit être mis dans la valeur de retour de la fonction actuelle, afin que les appelants sachent qu'une erreur pourrait en résulter.
Mais cela ne semble pas être l'approche suggérée par Microsoft.
L'utilisation de OneOf
au lieu d'exceptions pour gérer les exceptions "ordinaires" (comme Fichier introuvable, etc.) est-elle une approche raisonnable ou est-ce une pratique terrible?
Il est à noter que j'ai entendu dire que les exceptions comme flux de contrôle sont considérées comme un contre-modèle sérieux , donc si "l'exception" est quelque chose que vous géreriez normalement sans terminer le programme, n'est-ce pas "le flux de contrôle " dans un sens? Je comprends qu'il y a un peu de zone grise ici.
Notez que je n'utilise pas OneOf
pour des choses comme "Out Of Memory", les conditions dont je ne m'attends pas à récupérer vont toujours lever des exceptions. Mais je pense que les problèmes sont tout à fait raisonnables, comme les entrées utilisateur qui ne sont pas analysées sont essentiellement des "flux de contrôle" et ne devraient probablement pas lever d'exceptions.
Pensées suivantes:
De cette discussion, ce que je retiens actuellement est le suivant:
catch
et gère l'exception la plupart du temps et continue son travail, peut-être par un autre chemin, il devrait probablement faire partie du type de retour. Optional
ou OneOf
peuvent être utiles ici.Parse
et TryParse
.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 indéfini arrête le système car un système indéfini peut faire des choses désagréables comme des données corrompues, formater le disque dur et envoyer au président des courriels menaçants. Si vous ne pouvez pas récupérer et remettre le système dans un état défini, le plantage est la chose responsable à faire. C'est exactement pourquoi nous construisons des systèmes qui plantent plutôt que de se déchirer tranquillement. Maintenant, bien sûr, nous voulons tous un système stable qui ne se bloque jamais, mais nous ne le voulons vraiment que lorsque le système reste dans un état de sécurité prévisible défini.
J'ai entendu dire que les exceptions comme flux de contrôle sont considérées comme un contre-modèle sérieux
C'est absolument vrai, mais c'est souvent mal compris. Quand ils ont inventé le système d'exception, ils avaient peur de briser la programmation structurée. La programmation structurée est la raison pour laquelle nous avons for
, while
, until
, break
et continue
quand tout ce dont nous avons besoin, pour faire tout c'est goto
.
Dijkstra nous a appris que l'utilisation de goto de manière informelle (c'est-à-dire sauter partout où vous voulez) fait de la lecture du code un cauchemar. Quand ils nous ont donné le système d'exception, ils avaient peur de réinventer le goto. Ils nous ont donc dit de ne pas "l'utiliser pour le contrôle de flux" en espérant que nous comprendrions. Malheureusement, beaucoup d'entre nous ne l'ont pas fait.
Étrangement, nous n'utilisons pas souvent les exceptions pour créer du code spaghetti comme nous le faisions avec goto. Le conseil lui-même semble avoir causé plus de problèmes.
Fondamentalement, les exceptions concernent le rejet d'une hypothèse. Lorsque vous demandez qu'un fichier soit enregistré, vous supposez que le fichier peut et sera enregistré. L'exception que vous obtenez quand cela ne peut pas concerner le nom est illégale, la HD pleine, ou parce qu'un rat a rongé votre câble de données. Vous pouvez gérer toutes ces erreurs différemment, vous pouvez les gérer de la même manière ou les laisser arrêter le système. Il existe un chemin heureux dans votre code où vos hypothèses doivent rester vraies. D'une manière ou d'une autre, les exceptions vous éloignent de cette voie heureuse. Strictement parlant, oui, c'est une sorte de "contrôle de flux" mais ce n'est pas de cela qu'ils vous avertissaient. Ils parlaient de non-sens comme ça :
"Les exceptions devraient être exceptionnelles". Cette petite tautologie est née parce que les concepteurs de systèmes d'exception ont besoin de temps pour créer des traces de pile. Comparé au saut, c'est lent. Il mange du temps CPU. Mais si vous êtes sur le point de vous connecter et d'arrêter le système ou au moins d'arrêter le traitement intensif en cours avant de commencer le suivant, vous avez un peu de temps à tuer. 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ée comme une considération de performance.
Bien plus important que cela ne nous déroute pas. Combien de temps vous a-t-il fallu pour repérer la boucle infinie dans le code ci-dessus?
NE PAS renvoyer des codes d'erreur.
... est un bon conseil lorsque vous êtes dans une base de code qui n'utilise généralement pas de codes d'erreur. Pourquoi? Parce que personne ne se souviendra d'enregistrer la valeur de retour et de vérifier vos codes d'erreur. C'est quand même une belle convention quand tu es en C.
OneOf
Vous utilisez encore une autre convention. C'est bien tant que vous établissez la convention et pas simplement en combattez une autre. Il est déroutant d'avoir deux conventions d'erreur dans la même base de code. Si vous vous êtes débarrassé de tout le code qui utilise l'autre convention, allez-y.
J'aime moi-même la convention. L'une des meilleures explications que j'ai trouvées ici*:
Mais bien que je l'aime, je ne vais toujours pas le mélanger avec les autres conventions. Choisissez en un et gardez le.1
1: J'entends par là ne me faites pas penser à plus d'une convention à la fois.
Pensées ultérieures:
De cette discussion, ce que je retiens actuellement est le suivant:
- Si vous vous attendez à ce que l'appelant immédiat intercepte et gère l'exception la plupart du temps et continue son travail, peut-être par un autre chemin, il devrait probablement faire partie du type de retour. Facultatif ou OneOf peut être utile ici.
- Si vous vous attendez à ce que l'appelant immédiat n'attrape pas l'exception la plupart du temps, lancez une exception, pour éviter la sottise de la passer manuellement dans la pile.
- Si vous n'êtes pas sûr de ce que l'appelant immédiat va faire, fournissez peut-être les deux, comme Parse et TryParse.
Ce n'est vraiment pas si simple. L'une des choses fondamentales que vous devez comprendre est ce qu'est un zéro.
Combien de jours reste-t-il en mai? 0 (car 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 le chemin heureux. Mais si vous avez choisi des valeurs pour envoyer le chemin heureux qui signalent que les choses ne sont pas aussi simples qu'on le supposait, vous pouvez rester sur ce chemin aussi longtemps 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 mapper votre idée de rejet d'hypothèse. Vous pouvez reconnaître cette idée de son utilisation dans le bon vieux algèbre . Les monades peuvent aider à cela, mais ce ne doit pas toujours être une monade.
Par exemple2:
IList<int> ParseAllTheInts(String s) { ... }
Pouvez-vous penser à une bonne raison pour laquelle cela doit être conçu de manière à ce qu'il jette délibérément quelque chose? Devinez ce que vous obtenez quand aucun int ne peut être analysé? Je n'ai même pas besoin de te le dire.
C'est le signe d'un bon nom. Désolé mais TryParse n'est pas mon idée d'un bon nom.
Nous évitons souvent de lever une exception pour ne rien obtenir lorsque la réponse peut être plus d'une chose en même temps, mais pour une raison quelconque, si la réponse est une chose ou rien, nous sommes obsédés par le fait d'insister pour qu'elle nous donne une chose ou un lancer:
IList<Point> Intersection(Line a, Line b) { ... }
Les lignes parallèles doivent-elles vraiment provoquer une exception ici? Est-ce vraiment si mauvais que cette liste ne contienne jamais plus d'un point?
Peut-être que sémantiquement vous ne pouvez tout simplement pas accepter cela. Si c'est le cas, c'est dommage. Mais peut-être que les monades, qui n'ont pas une taille arbitraire comme List
, vous feront vous sentir mieux à ce sujet.
Maybe<Point> Intersection(Line a, Line b) { ... }
Les Monades sont de petites collections à usage spécial qui sont destinées à être utilisées de manière spécifique qui évitent d'avoir à les tester. Nous sommes censés trouver des moyens de les gérer indépendamment de ce qu'ils contiennent. De cette façon, le chemin heureux reste simple. Si vous vous ouvrez et testez chaque Monade que vous touchez, vous les utilisez mal.
Je sais, c'est bizarre. Mais c'est un nouvel outil (enfin, pour nous). Alors donnez-lui un peu de temps. Les marteaux ont plus de sens lorsque vous cessez de les utiliser sur des vis.
Si vous me faites plaisir, je voudrais adresser ce commentaire:
Comment se fait-il qu'aucune des réponses ne précise que la monade Either n'est pas un code d'erreur, ni OneOf non plus? Ils sont fondamentalement différents, et la question semble donc reposer sur un malentendu. (Bien que sous une forme modifiée, c'est toujours une question valide.) - Konrad Rudolph4 juin 18 à 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 font de beaux contenants pour de telles choses lorsqu'ils sont utilisés à bon escient.
C # n'est pas Haskell, et vous devez 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, éventuellement, vous découvrirez probablement les raisons pour lesquelles la communauté C # fait les choses différemment. L'une des principales raisons est que C # ne prend pas en charge les syndicats discriminés.
J'ai entendu dire que les exceptions en tant que flux de contrôle sont considérées comme un contre-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 lever une exception (que l'appelant est libre de ne pas gérer) ou de renvoyer une valeur composée (que l'appelant DOIT gérer).
La propagation des conditions d'erreur vers le haut à travers la pile d'appels nécessite un conditionnel à chaque niveau, doublant la complexité cyclomatique de ces méthodes et par conséquent doublant le nombre de cas de tests unitaires. Dans les applications d'entreprise typiques, de nombreuses exceptions sont au-delà de la récupération et peuvent être laissées se propager au plus haut niveau (par exemple, le point d'entrée de service d'une application Web).
Vous êtes arrivé à C # à un moment intéressant. Jusqu'à relativement 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 de la correspondance des modèles). Assez raisonnablement, les conseils officiels de Microsoft étaient d'éviter les codes de retour et d'utiliser des exceptions.
Cependant, il y a toujours eu des exceptions, sous la forme de méthodes TryXXX
, qui renverraient un résultat de réussite boolean
et fourniraient un deuxième résultat de valeur via un paramètre out
. Celles-ci sont très similaires aux modèles "try" dans la programmation fonctionnelle, sauf que le résultat provient d'un paramètre out, plutôt que d'un Maybe<T>
valeur de retour.
Mais les choses changent. La programmation fonctionnelle devient de plus en plus populaire et les langages comme C # répondent. C # 7.0 a introduit quelques fonctionnalités de base de correspondance de modèles dans le langage; C # 8 introduira beaucoup plus, y compris une expression de commutateur, des motifs récursifs etc. Parallèlement à cela, il y a eu une croissance de "bibliothèques fonctionnelles" pour C #, comme ma propre bibliothèque Succinc <T> , qui soutient également les syndicats discriminés.
Ces deux choses combinées signifient que le code comme celui-ci gagne lentement en popularité.
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 un créneau pour le moment, même si au cours des deux dernières années, j'ai remarqué un changement marqué d'une hostilité générale parmi les développeurs C # à un tel code à un intérêt croissant pour l'utilisation de ces techniques.
Nous ne sommes pas encore là pour dire " NE PAS renvoyer les codes d'erreur." est désuet et doit être retiré. Mais la marche vers cet objectif est en bonne voie. Alors faites votre choix: respectez les anciennes façons de lever les exceptions avec l'abandon gay si c'est la façon dont vous aimez faire les choses; ou commencez à explorer un nouveau monde où les conventions des langages fonctionnels deviennent plus populaires en C #.
Je pense qu'il est parfaitement raisonnable d'utiliser un DU pour renvoyer des erreurs, mais je dirais que comme j'ai écrit OneOf :) Mais je dirais que de toute façon, car c'est une pratique courante en F # etc (par exemple le type de résultat, comme mentionné par d'autres ).
Je ne pense pas que les erreurs que je retourne généralement comme des situations exceptionnelles, mais plutôt comme des situations normales, mais j'espère rarement rencontrées, qui peuvent ou ne doivent pas ê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;
}
La levée d'exceptions pour ces erreurs semble exagérée - elles ont un surcoût de performance, à part les inconvénients de ne pas être explicites 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 enregistrer des exceptions pour les choses qui indiquent que quelque chose est cassé dans la mesure du possible.
L'utilisation de OneOf au lieu d'exceptions pour gérer les exceptions "ordinaires" (comme Fichier introuvable, etc.) est-elle une approche raisonnable ou est-ce une pratique terrible?
Il convient de noter que j'ai entendu dire que les exceptions en tant que flux de contrôle sont considérées comme un contre-modèle sérieux, donc si l '"exception" est quelque chose que vous géreriez normalement sans terminer le programme, n'est-ce pas ce "flux de contrôle" en quelque sorte?
Les erreurs ont un certain nombre de dimensions. J'identifie trois dimensions: peuvent-elles être évitées, à quelle fréquence elles se produisent 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 pour laisser l'erreur "discrètement" bouillonner comme 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 au mieux de forcer l'appelant à les affronter. En Java, je leur fais des exceptions vérifiées, et un effet similaire est obtenu en les faisant Try*
méthodes ou retour d'une union discriminée. Les unions discriminées sont particulièrement agréables lorsqu'une fonction a une valeur de retour. Ils sont une version beaucoup plus raffinée du retour de null
. La syntaxe de try/catch
blocs n'est pas génial (trop de crochets), ce qui rend les alternatives plus belles. De plus, les exceptions sont légèrement lentes car elles enregistrent les traces de pile.
Les erreurs évitables (qui n'ont pas été évitées en raison d'une erreur/négligence du programmeur) et les erreurs non récupérables fonctionnent très bien comme exceptions ordinaires. Les erreurs peu fréquentes fonctionnent également mieux comme exceptions ordinaires, car un programmeur peut souvent peser 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 d'une méthode s'inscrivent dans ces dimensions, ce qui doit être gardé à l'esprit lors de l'examen des éléments suivants.
D'après mon expérience, forcer l'appelant à faire face à des conditions d'erreur est correct lorsque les erreurs ne sont pas évitables, fréquentes et récupérables, mais cela devient très ennuyeux lorsque l'appelant est forcé de sauter à travers des cerceaux lorsqu'ils n'en avez pas besoin ou ne le souhaitez pas . Cela peut être dû au fait que l'appelant sait que l'erreur ne se produira pas ou parce qu'il ne souhaite pas (encore) rendre le code résilient. En Java, cela explique l'angoisse contre l'utilisation fréquente des exceptions vérifiées et le retour facultatif (qui est similaire à Peut-être et a été récemment introduit au milieu de nombreuses controverses). En cas de doute, lancez des exceptions et laissez l'appelant décider de la manière dont il veut les gérer.
Enfin, gardez à l'esprit que la chose la plus importante concernant les conditions d'erreur, bien au-dessus de toute autre considération telle que la façon dont elles sont signalées, est de documentez-les soigneusement.
OneOf
est à peu près équivalent aux exceptions vérifiées en Java. Il y a quelques différences:
OneOf
ne fonctionne pas avec les méthodes produisant des méthodes appelées pour leurs effets secondaires (car elles peuvent être significativement appelées et le résultat simplement ignoré). Évidemment, nous devrions tous essayer d'utiliser des fonctions pures, mais ce n'est pas toujours possible.
OneOf
ne fournit aucune information sur l'origine du problème. C'est bon car cela permet d'économiser les performances (les exceptions levées dans Java sont coûteuses en raison du remplissage de la trace de la pile, et en C # ce sera la même)) et vous oblige à fournir suffisamment d'informations dans l'erreur membre de OneOf
lui-même. Il est également mauvais car ces informations peuvent être insuffisantes et vous pouvez avoir du mal à trouver l'origine du problème.
OneOf
et l'exception vérifiée ont une "fonction" importante en commun:
Cela empêche votre peur
En C #, ignorer les exceptions ne produit pas d'erreur de compilation, et si elles ne sont pas interceptées, elles bouillonnent et plantent votre programme.
mais comme déjà dit, cette peur est irrationnelle. Fondamentalement, il y a généralement un seul endroit dans votre programme, où vous devez tout attraper (il y a de fortes chances 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, n'est-ce pas?
Le gros avantage des exceptions ignorables est qu'elles peuvent être gérées où vous voulez les gérer, plutôt que partout dans la trace de la pile. Cela élimine des tonnes de passe-partout (il suffit de regarder certains Java déclarant "jette ...." ou en mettant des exceptions) et cela rend également votre code moins bogué: comme n'importe quelle méthode peut lancer, vous avez besoin Heureusement, la bonne action ne fait généralement rien, c'est-à-dire le laisser bouillonner jusqu'à un endroit où il peut être raisonnablement manipulé.
Les autres réponses ont discuté des exceptions par rapport aux codes d'erreur de manière suffisamment détaillée, donc je voudrais ajouter une autre perspective spécifique à la question:
La façon dont il est utilisé, OneOf<Value, Error1, Error2>
c'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, il peut fournir plus de détails pourquoi.
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 lorsque 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 avez un problème et les exceptions seraient le meilleur choix. Comparez Rail save
vs save!
.
Une chose que vous devez 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 ne se soucie pas de ce que vous faites avec la valeur de retour d'une fonction, votre approche peut vous causer un chagrin considérable. Par exemple, si vous avez une méthode qui supprime un fichier, vous voulez vous assurer que l'application remarque quand la méthode a échoué, même s'il n'y a aucune raison de vérifier un code d'erreur "sur le chemin heureux". Il s'agit d'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 accesseurs de propriété IIRC).
La raison pour laquelle MSDN vous dit de "ne pas renvoyer de codes d'erreur" est exactement la suivante: 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 en toute sécurité utilisez 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 non plus le "bon résultat". Il y a plusieurs façons de le faire - par exemple, vous ne pouvez autoriser la "lecture" de la valeur qu'en passant un délégué pour gérer le succès et le des cas d'erreur, et vous pouvez facilement combiner cela avec des approches comme Railway Oriented Programming (cela fonctionne très bien en C #).
int.TryParse
est quelque part au milieu. C'est pur. Il définit à tout moment les valeurs des deux résultats. Vous savez donc que si la valeur de retour 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 vous sauvera pas si quelqu'un change cette fonction pour avoir des effets secondaires. Ou pour lever des exceptions. Donc, bien que cette approche soit parfaitement adaptée en C # (et je l'utilise), vous devez vous assurer que tout le monde dans votre équipe le comprend et l'utilise . De nombreux 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 vous-même. Vous devez vous tenir au courant des choses qui cassent si vous ne suivez pas les règles que Haskell applique par défaut - et c'est quelque chose que vous gardez à l'esprit pour tous les paradigmes fonctionnels que vous introduisez dans votre code.
L'instruction correcte serait N'utilisez pas de codes d'erreur pour les exceptions! Et n'utilisez pas d'exceptions pour les non-exceptions.
S'il se passe quelque chose que votre code ne peut pas récupérer (c'est-à-dire faire fonctionner), c'est une exception. Il n'y a rien que votre code immédiat ferait avec un code d'erreur, sauf le remettre vers le haut pour vous connecter ou peut-être fournir le code à l'utilisateur d'une manière ou d'une autre. Cependant, c'est exactement à cela que servent les exceptions - elles traitent de toutes les remises et aident à analyser ce qui s'est réellement passé.
Si toutefois quelque chose se produit, comme une entrée non valide que vous pouvez gérer, soit en la reformatant automatiquement, soit en demandant à l'utilisateur de corriger les données (et vous avez pensé à ce cas), ce n'est pas vraiment une exception en premier lieu - comme vous attendez-le. Vous pouvez vous sentir libre de gérer ces cas avec des codes d'erreur ou toute autre architecture. Mais pas d'exceptions.
(Notez que je ne suis pas très expérimenté en C #, mais ma réponse devrait tenir dans le cas général en fonction du niveau conceptuel de ce que sont les exceptions.)