Je finis parfois par écrire une méthode ou une propriété pour une bibliothèque de classes pour laquelle il n'est pas exceptionnel de ne pas avoir de vraie réponse, mais un échec. Quelque chose ne peut pas être déterminé, n'est pas disponible, n'a pas été trouvé, n'est pas possible actuellement ou il n'y a plus de données disponibles.
Je pense qu'il existe trois solutions possibles pour une situation aussi peu exceptionnelle pour indiquer un échec en C # 4:
- renvoyer une valeur magique qui n'a pas d'autre sens (comme
null
et-1
); - lancer une exception (par exemple
KeyNotFoundException
); - return
false
et fournit la valeur de retour actuelle dans unout
paramètre (tel queDictionary<,>.TryGetValue
).
Les questions sont donc les suivantes: dans quelle situation non exceptionnelle devrais-je lancer une exception? Et si je ne devais pas jeter: quand est-ce que le retour d’une valeur magique indiquée plus haut implémente-t-il une Try*
méthode avec un out
paramètre ? (Pour moi, le out
paramètre semble sale et il est plus fastidieux de l'utiliser correctement.)
Je cherche des réponses factuelles, telles que des réponses impliquant des directives de conception (je ne connais aucune Try*
méthode), une convivialité (comme je le demande pour une bibliothèque de classe), une cohérence avec la BCL et une lisibilité.
Dans la bibliothèque de classes .NET Framework Base, les trois méthodes sont utilisées:
- retourne une valeur magique qui n'a pas de sens sinon:
Collection<T>.IndexOf
renvoie -1,StreamReader.Read
renvoie -1,Math.Sqrt
renvoie NaN,Hashtable.Item
renvoie null;
- jeter une exception:
Dictionary<,>.Item
lève KeyNotFoundException,Double.Parse
jette FormatException; ou
- renvoyer
false
et fournir la valeur de retour actuelle dans unout
paramètre:
Notez que, comme il a Hashtable
été créé à l’époque où il n’existait pas de génériques en C #, il utilise object
et peut donc être renvoyé null
comme une valeur magique. Mais avec les génériques, des exceptions sont utilisées Dictionary<,>
et, au départ, ce n’était pas le cas TryGetValue
. Apparemment, les idées changent.
De toute évidence, la Item
- TryGetValue
et Parse
- TryParse
dualité existe pour une raison. Je suppose donc que le lancement d’exceptions pour les échecs non exceptionnels n’est pas effectué en C # 4 . Cependant, les Try*
méthodes n'existaient pas toujours, même lorsqu'elles Dictionary<,>.Item
existaient.
la source
Réponses:
Je ne pense pas que vos exemples soient vraiment équivalents. Il existe trois groupes distincts, chacun ayant sa propre raison de se comporter.
StreamReader.Read
ou lorsqu'il existe une valeur simple à utiliser qui ne sera jamais une réponse valide (-1 pourIndexOf
).Les exemples que vous fournissez sont parfaitement clairs pour les cas 2 et 3. Pour les valeurs magiques, on peut dire s’il s’agit d’une bonne décision de conception ou non dans tous les cas.
Le
NaN
retour parMath.Sqrt
est un cas particulier - il suit la norme de virgule flottante.la source
Either<TLeft, TRight>
monade?Vous essayez de communiquer à l'utilisateur de l'API ce qu'il doit faire. Si vous lancez une exception, rien ne l'oblige à l'attraper et la seule lecture de la documentation leur permettra de connaître toutes les possibilités. Personnellement, je trouve qu'il est lent et fastidieux de fouiller dans la documentation pour trouver toutes les exceptions qu'une méthode peut générer (même si c'est dans intellisense, je dois encore les copier manuellement).
Une valeur magique nécessite toujours que vous lisiez la documentation et, éventuellement, référenciez une
const
table pour décoder la valeur. Au moins, il n’ya pas de surcharge d’une exception pour ce que vous appelez un événement non exceptionnel.C'est pourquoi, même si les
out
paramètres sont parfois mal vus, je préfère cette méthode, avec laTry...
syntaxe. C'est la syntaxe canonique .NET et C #. Vous indiquez à l'utilisateur de l'API qu'il doit vérifier la valeur de retour avant d'utiliser le résultat. Vous pouvez également inclure un deuxièmeout
paramètre avec un message d'erreur utile, ce qui facilite encore le débogage. C'est pourquoi je vote pour la solutionTry...
avecout
paramètre.Une autre option consiste à retourner un objet "résultat" spécial, bien que cela me paraisse beaucoup plus fastidieux:
Ensuite, votre fonction a l’ air juste car elle n’a que des paramètres d’entrée et ne renvoie qu’une chose. C'est juste que la seule chose qu'il retourne est une sorte de tuple.
En fait, si vous aimez ce genre de choses, vous pouvez utiliser les nouvelles
Tuple<>
classes introduites dans .NET 4. Personnellement, je n'aime pas le fait que la signification de chaque champ soit moins explicite, car je ne peux pas donnerItem1
etItem2
noms utiles.la source
IMyResult
car il est possible de communiquer un résultat plus complexe que justetrue
oufalse
ou la valeur du résultat.Try*()
n'est utile que pour des choses simples comme la conversion de chaîne en entier.Comme vos exemples le montrent déjà, chaque cas de ce type doit être évalué séparément et il existe un large spectre de gris entre les "circonstances exceptionnelles" et le "contrôle de flux", en particulier si votre méthode est censée être réutilisable et peut être utilisée de manières très différentes. qu'il a été conçu à l'origine pour. Ne vous attendez pas à ce que tous ici s'entendent sur ce que signifie "non exceptionnel", surtout si vous discutez immédiatement de la possibilité d'utiliser des "exceptions" pour mettre cela en œuvre.
Nous pourrions également ne pas être d’accord sur la conception qui rend le code le plus facile à lire et à maintenir, mais je suppose que le concepteur de la bibliothèque en a une vision personnelle claire et qu’il ne lui faut que l’équilibrer par rapport aux autres considérations en jeu.
Réponse courte
Suivez vos instincts sauf lorsque vous concevez des méthodes assez rapides et attendez-vous à une possibilité de réutilisation imprévue.
Longue réponse
Chaque futur appelant peut librement faire la traduction entre les codes d'erreur et les exceptions comme il le souhaite dans les deux sens; Cela rend les deux approches de conception presque équivalentes, à l'exception des performances, de la convivialité du débogueur et de certains contextes d'interopérabilité restreinte. Cela se résume généralement à la performance, alors concentrons-nous là-dessus.
En règle générale, attendez-vous à ce qu'une exception soit générée 200 fois plus lentement qu'un retour régulier (en réalité, il y a un écart important).
En règle générale, le lancement d'une exception peut souvent permettre un code beaucoup plus propre comparé à la plus grossière des valeurs magiques, car vous ne comptez pas sur le programmeur qui traduit le code d'erreur en un autre code d'erreur, car il parcourt plusieurs couches de code client vers un point où il y a suffisamment de contexte pour le gérer de manière cohérente et adéquate. (Cas particulier: a
null
tendance à être meilleur ici que d’autres valeurs magiques en raison de sa tendance à se traduire automatiquementNullReferenceException
en cas de défauts, mais pas tous; types de défauts; généralement, mais pas toujours, assez proches de la source du défaut. )Alors, quelle est la leçon?
Pour une fonction appelée quelques fois au cours de la durée de vie d'une application (comme son initialisation), utilisez tout ce qui vous donne un code plus propre et plus facile à comprendre. La performance ne peut pas être une préoccupation.
Pour une fonction jetable, utilisez tout ce qui vous donne un code plus propre. Effectuez ensuite un profilage (si nécessaire) et modifiez les exceptions en codes de retour s’ils font partie des goulets d’étranglement présumés fondés sur des mesures ou la structure globale du programme.
Pour une fonction réutilisable coûteuse, utilisez tout ce qui vous donne un code plus propre. Si, fondamentalement, vous devez toujours subir un aller-retour sur le réseau ou analyser un fichier XML sur disque, le temps système nécessaire au lancement d'une exception est probablement négligeable. Il est plus important de ne pas perdre les détails de toute défaillance, même accidentellement, que de revenir très rapidement d'une "défaillance non exceptionnelle".
Une fonction maigre réutilisable nécessite plus de réflexion. En utilisant des exceptions, vous forcez quelque chose comme un ralentissement de 100 fois sur les appelants qui verront l'exception sur la moitié de leurs (nombreux) appels, si le corps de la fonction s'exécute très rapidement. Les exceptions constituent toujours une option de conception, mais vous devrez fournir une alternative peu onéreuse aux appelants qui ne peuvent se le permettre. Regardons un exemple.
Vous avez énuméré un excellent exemple de
Dictionary<,>.Item
ce qui, en gros, est passé du renvoi denull
valeurs au jetéKeyNotFoundException
entre .NET 1.1 et .NET 2.0 (uniquement si vous êtes prêt à considérerHashtable.Item
son précurseur non générique pratique). La raison de ce "changement" n’est pas sans intérêt ici. L’optimisation des performances des types de valeur (plus de boxe) a fait de la valeur magique initiale (null
) une non-option;out
les paramètres ne rapportent qu’une petite partie des coûts de performance. Cette dernière considération de performance est totalement négligeable par rapport à la surcharge de lancer unKeyNotFoundException
, mais la conception des exceptions est toujours supérieure ici. Pourquoi?Contains
avant tout appel de l'indexeur, et ce modèle se lit de manière totalement naturelle. Si un développeur souhaite appeler mais oublie d'appelerContains
, aucun problème de performances ne peut s'y glisser;KeyNotFoundException
est assez fort pour être remarqué et corrigé.la source
Contains
avant un appel à un indexeur peut provoquer une condition de concurrence qui ne doit pas nécessairement être présenteTryGetValue
.ConcurrentDictionary
pour un accès multithread etDictionary
pour un accès à un seul thread pour des performances optimales. Autrement dit, ne pas utiliser `` Contains` ne rend PAS le thread de code sûr avec cetteDictionary
classe particulière .Vous ne devriez pas permettre l'échec.
Je sais, c'est vague et idéaliste, mais écoute-moi. Lors de la conception, il existe un certain nombre de cas dans lesquels vous avez la possibilité de privilégier une version ne comportant aucun mode de défaillance. Au lieu d'avoir un 'FindAll' qui échoue, LINQ utilise une clause where qui renvoie simplement un énumérable vide. Au lieu d'avoir un objet qui doit être initialisé avant d'être utilisé, laissez le constructeur initialiser l'objet (ou initialiser lorsque le non-initialisé est détecté). La clé est la suppression de la branche de défaillance dans le code du consommateur. C'est le problème, alors concentrez-vous dessus.
Une autre stratégie pour cela est le
KeyNotFound
scénario. Dans presque toutes les bases de code sur lesquelles j'ai travaillé depuis la version 3.0, il existe quelque chose de similaire à cette méthode d'extension:Il n'y a pas de véritable mode de défaillance pour cela.
ConcurrentDictionary
a un similaireGetOrAdd
construit en.Cela dit, il y aura toujours des moments où c'est tout simplement inévitable. Tous trois ont leur place, mais je privilégierais la première option. En dépit de tout ce qui est fait du danger de null, il est bien connu et s'inscrit dans bon nombre des scénarios «élément introuvable» ou «résultat non applicable» qui composent l'ensemble «défaillance non exceptionnelle». En particulier lorsque vous créez des types de valeur nullable, la signification de «cela peut échouer» est très explicite dans le code et difficile à oublier / à gâcher.
La deuxième option est suffisante lorsque votre utilisateur fait quelque chose de stupide. Vous donne une chaîne avec un format incorrect, essaie de fixer la date au 42 décembre ... quelque chose que vous voulez que les choses explosent rapidement et de façon spectaculaire lors des tests afin que le mauvais code soit identifié et corrigé.
La dernière option est celle que je déteste de plus en plus. Les paramètres Out sont délicats et tendent à violer certaines des meilleures pratiques lors de la création de méthodes, comme se concentrer sur une chose et ne pas avoir d’effets secondaires. De plus, le paramètre out n’est généralement significatif que lorsqu’il réussit. Cela dit, elles sont essentielles pour certaines opérations qui sont généralement limitées par des problèmes de simultanéité ou des problèmes de performances (pour lesquels vous ne souhaitez pas effectuer un deuxième voyage dans la base de données, par exemple).
Si la valeur de retour et le paramètre out ne sont pas triviaux, la suggestion de Scott Whitlock concernant un objet de résultat est préférée (comme la
Match
classe de Regex ).la source
out
paramètres sont complètement orthogonaux à la question des effets secondaires. La modification d'unref
paramètre est un effet secondaire et la modification de l'état d'un objet que vous transmettez via un paramètre d'entrée est un effet secondaire, mais unout
paramètre n'est qu'un moyen peu pratique de faire en sorte qu'une fonction renvoie plus d'une valeur. Il n'y a pas d'effet secondaire, juste plusieurs valeurs de retour.Toujours préférer lancer une exception. Il possède une interface uniforme entre toutes les fonctions susceptibles d’échouer, et indique les échecs aussi bruyamment que possible - une propriété très souhaitable.
Notez que
Parse
etTryParse
ne sont pas vraiment la même chose en dehors des modes de défaillance. Le fait queTryParse
la valeur puisse également être renvoyée est en quelque sorte orthogonal. Prenons le cas où, par exemple, vous validez une entrée. Vous ne vous souciez pas réellement de la valeur, tant qu'elle est valide. Et il n'y a rien de mal à offrir une sorte deIsValidFor(arguments)
fonction. Mais il ne peut jamais être le principal mode de fonctionnement.la source
Comme d'autres l'ont fait remarquer, la valeur magique (y compris une valeur de retour booléenne) n'est pas une excellente solution, sauf en tant que marqueur de "fin de plage". Raison: la sémantique n'est pas explicite, même si vous examinez les méthodes de l'objet. Vous devez en fait lire la documentation complète de l'objet entier jusqu'à "oh ouais, s'il renvoie -42, cela signifie bla bla bla".
Cette solution peut être utilisée pour des raisons historiques ou pour des raisons de performances, mais doit sinon être évitée.
Cela laisse deux cas généraux: probing ou exceptions.
Ici, la règle de base est que le programme ne doit pas réagir aux exceptions, sauf pour gérer lorsque le programme / involontairement / viole une condition. Le sondage devrait être utilisé pour s'assurer que cela n'arrive pas. Par conséquent, une exception signifie soit que le sondage pertinent n’a pas été effectué à l’avance, soit que quelque chose de tout à fait inattendu s’est passé.
Exemple:
Vous voulez créer un fichier à partir d'un chemin donné.
Vous devez utiliser l'objet File pour déterminer à l'avance si ce chemin est légal pour la création ou l'écriture de fichier.
Si votre programme finit toujours par essayer d'écrire sur un chemin illégal ou illisible, vous devriez obtenir une excaption. Cela peut se produire en raison d'une situation de concurrence critique (un autre utilisateur a supprimé le répertoire ou l'a rendu en lecture seule après avoir rencontré des problèmes).
La tâche consistant à gérer une défaillance inattendue (signalée par une exception) et à vérifier si les conditions sont correctes pour l'opération à l'avance (vérification) sera généralement structurée différemment et devrait donc utiliser des mécanismes différents.
la source
Je pense que le
Try
modèle est le meilleur choix, quand le code indique simplement ce qui s'est passé. Je déteste param et comme objet nullable. J'ai créé le cours suivantafin que je puisse écrire le code suivant
Ressemble à nullable mais pas seulement pour le type de valeur
Un autre exemple
la source
try
catch
fera des ravages sur votre performance si vous appelezGetXElement()
et échouez plusieurs fois.public struct Nullable<T> where T : struct
la différence principale dans une contrainte. BTW la dernière version ici est github.com/Nelibur/Nelibur/blob/master/Source/Nelibur.Sword/...Si vous êtes intéressé par l'itinéraire "valeur magique", une autre solution consiste à surcharger l'objectif de la classe Lazy. Bien que Lazy ait pour but de différer l’instanciation, rien ne vous empêche réellement d’utiliser une option similaire à Maybe ou Option. Par exemple:
la source