Quelle est la façon la plus élégante d'écrire une méthode «Try» en C # 7?


21

J'écris un type d'implémentation de file d'attente qui a une TryDequeueméthode qui utilise un modèle similaire à diverses TryParseméthodes .NET , où je retourne une valeur booléenne si l'action a réussi, et utilise un outparamètre pour renvoyer la valeur réelle retirée de la file d'attente.

public bool TryDequeue(out Message message) => _innerQueue.TryDequeue(out message);

Maintenant, j'aime éviter les outparamètres chaque fois que je le peux. C # 7 nous donne des délocalisations variables pour faciliter le travail avec eux, mais je considère toujours que les paramètres sont plus un mal nécessaire qu'un outil utile.

Le comportement que je veux de cette méthode est le suivant:

  • S'il y a un article à retirer, renvoyez-le.
  • S'il n'y a aucun élément à retirer (la file d'attente est vide), fournissez à l'appelant suffisamment d'informations pour agir correctement.
  • Ne renvoyez pas simplement un élément nul s'il ne reste aucun élément.
  • Ne lancez pas d'exception si vous essayez de retirer la file d'attente d'une file d'attente vide.

À l'heure actuelle, un appelant de cette méthode utilise presque toujours un modèle comme le suivant (en utilisant la syntaxe de variable C # 7 out):

if (myMessageQueue.TryDequeue(out Message dequeued))
    MyMessagingClass.SendMessage(dequeued)
else
    Console.WriteLine("No messages!"); // do other stuff

Ce qui n'est pas le pire, tout compte fait. Mais je ne peux pas m'empêcher de penser qu'il pourrait y avoir de meilleures façons de le faire (je suis tout à fait disposé à admettre qu'il pourrait ne pas y en avoir). Je déteste la façon dont l'appelant doit rompre son flux avec un conditionnel quand tout ce qu'il veut, c'est obtenir une valeur s'il en existe une.

Quels sont les autres modèles qui existent pour accomplir ce même comportement "essayer"?

Pour le contexte, cette méthode peut potentiellement être appelée dans les projets VB, donc des points bonus pour quelque chose qui fonctionne bien dans les deux. Ce fait devrait cependant avoir très peu de poids.


9
Définissez une Option<T>structure et renvoyez-la. Ces bool Try(..., out data)fonctions sont une abomination.
CodesInChaos

2
Je pensais similaire ... Peut-être <T>, Option <T> si l'on est contraire aux paramètres OUT.
Jon Raynor

1
@FrustratedWithFormsDesigner bien, je n'ai pas tellement "essayé" d'autres choses (les jeux de mots sont les bienvenus), autant que j'y ai pensé. J'ai eu l'idée de retourner un ValueTuple mais au mieux je ne pense pas que cela ait apporté beaucoup d'amélioration.
Eric Sondergard

@CodesInChaos Nous sommes sur la même longueur d'onde, c'est pourquoi je suis ici! Et j'aime cette idée. Si vous avez le temps, voudriez-vous donner plus de détails dans une réponse afin que je puisse l'accepter?
Eric Sondergard

Réponses:


24

Utilisez un type Option, c'est-à-dire un objet d'un type qui a deux versions, généralement appelé "Some" (lorsqu'une valeur est présente) ou "None" (quand il n'y a pas de valeur) ... Ou occasionnellement ils s'appellent Just and Nothing. Il existe ensuite des fonctions dans ces types qui vous permettent d'accéder à la valeur si elle est présente, de tester la présence et, surtout, une méthode qui vous permet d'appliquer une fonction qui renvoie une option supplémentaire à la valeur si elle est présente (généralement en C # cela devrait être appelé FlatMap bien que dans d'autres langages, il soit souvent appelé Bind à la place ... Le nom est critique en C # parce que la méthode s de ce nom et de ce type vous permet d'utiliser vos objets Option dans les instructions LINQ).

Des fonctionnalités supplémentaires peuvent inclure des méthodes telles que IfPresent et IfNotPresent pour appeler des actions dans les conditions pertinentes, et OrElse (qui substitue une valeur par défaut lorsqu'aucune valeur n'est présente mais est un non op sinon), et ainsi de suite.

Votre exemple pourrait alors ressembler à ceci:

myMessageQueue.TryDeque()
    .IfPresent( dequeued => MyMessagingClass.SendMessage(dequeued))
    .IfNotPresent (() =>  Console.WriteLine("No messages!")

Il s'agit du modèle de monade Option (ou Peut-être), et il est extrêmement utile. Il existe des implémentations existantes (par exemple https://github.com/nlkl/Optional/blob/master/README.md ) mais ce n'est pas difficile non plus pour vous-même.

(Vous souhaiterez peut-être étendre ce modèle afin de renvoyer une description de la cause de l'erreur au lieu de rien lorsque la méthode échoue ... Ceci est entièrement réalisable et est souvent appelé la monade Either; comme son nom l'indique, vous pouvez utiliser le même FlatMap modèle pour le rendre facile à travailler avec dans ce cas aussi)


C'est certainement une bonne option, merci pour la suggestion et vos réflexions. Je cherchais vraiment à explorer plus d'idées ici.
Eric Sondergard

2
La bonne chose à propos de Option/ Maybeest qu'il s'agit d'une monade (si elle est implémentée en tant que telle, bien sûr), ce qui signifie qu'elle peut être chaînée, enveloppée, déballée et traitée en toute sécurité sans jamais avoir à gérer directement les différents cas, et ce chaînage et le traitement peut être effectué avec LINQ Query Expressions.
Jörg W Mittag

3
Par ailleurs, flatMap/ bindest appelé SelectManydans .NET.
Jörg W Mittag

5
Je me demande si ce serait une meilleure idée de choisir un nom différent, car en faisant cela, vous violez Try...les conventions de dénomination dans .NET (et risquez de confondre les responsables).
Bob

@Bob C'est un excellent point. Je suis d'accord.
Eric Sondergard

12

Les réponses données sont bonnes et j'irais avec l'une d'entre elles. Considérez cette réponse comme remplissant simplement quelques coins avec quelques idées alternatives:

  • Faites des sous-classes de Messageappelés SomeMessageet NoMessage. Dequeuepeut maintenant retourner NoMessages'il n'y a pas de message et SomeMessages'il y a un message. Si l'appelé se soucie de détecter dans quel cas il se trouve, il peut facilement le faire par inspection de type. S'ils ne le font pas, eh bien, faites juste unNoMessage faites rien quand une de ses méthodes est appelée, et hé, ils obtiennent ce qu'ils ont demandé.

  • Identique à ce qui précède, mais rend Messageimplicitement convertible en bool(ou implémente operator true / operator false). Maintenant, vous pouvez dire if (message)et faire en sorte que le message soit bon et faux s'il est mauvais. (C'est presque certainement une mauvaise idée, mais je l'inclus pour être complet!)

  • C # 7 a des tuples. Retourne un (bool, Message)tuple.

  • Faites Messageun type struct et retournez Message?.


Hé, merci pour votre réponse. Avec cette question, j'essayais tout autant de m'exposer à des idées différentes que j'essayais de trouver une solution à mon problème spécifique. À votre santé!
Eric Sondergard

Je veux dire, si votre "mauvaise idée" est utilisée par Unity, pourquoi ne pouvons-nous pas l'utiliser?
Arturo Torres Sánchez

@ ArturoTorresSánchez: Votre question est "quelqu'un d'autre a utilisé une très mauvaise pratique de programmation, alors pourquoi pas moi?" La question est incohérente. Personne ne vous empêche d'utiliser les mêmes mauvaises pratiques de programmation que quelqu'un d'autre. Allez-y. Cela n'en fera pas une bonne pratique en C #.
Eric Lippert

C'était la langue dans la joue. Je suppose que je dois utiliser "/ s" pour le marquer.
Arturo Torres Sánchez

@ ArturoTorresSánchez: Ceci est un site de questions et réponses; Je considère que les questions sont des questions qui cherchent des réponses .
Eric Lippert

10

En C # 7, vous pouvez utiliser la correspondance de motifs pour obtenir les mêmes résultats de manière plus élégante:

if (myMessageQueue.TryDequeue() is Message dequeued) 
{
     MyMessagingClass.SendMessage(dequeued)
} 
else 
{
    Console.WriteLine("No messages!"); // do other stuff
}

Dans ce cas particulier, j'utiliserais probablement des événements plutôt que d'interroger la file d'attente à plusieurs reprises.


3
Cela ne nécessite-t-il pas cependant TryDequeuede renvoyer une interface de marqueur avec une Messageimplémentation et une implémentation de "rien"? Bien sûr, c'est plus élégant sur le site d'appel, mais vous êtes bloqué en implémentant 3 implémentations dans le code réel, qui lui-même peut être impossible s'il Messages'agit d'un type existant.
Telastyn

1
@Telastyn: Cela fonctionne également s'il retourne simplement null . Vous pouvez également utiliser un type d'option.
JacquesB

@JacquesB: mais que faire si null est un élément pouvant être mis en file d'attente valide?
JBSnorro

5

Il n'y a rien de mal à une méthode Try ... (ayant un paramètre out) pour un scénario dans lequel l'échec (aucune valeur) est tout aussi courant que le succès.

Mais, si vous insistez pour faire les choses différemment, vous voudrez peut-être renvoyer un message vide et soit l'envoyer (ce n'est plus votre problème) ou reporter l'instruction if jusqu'à ce que vous deviez vraiment savoir si vous avez un message avec du contenu ou non.

Notez que quelqu'un doit faire le test de toute façon. Je dirais que le test devrait être fait quand et où la question est actuelle. C'est au moment de la déque.


Vous faites valoir un bon argument. Vous devez toujours tester, quoi que vous fassiez. Honnêtement, je peux encore utiliser des paramètres à ce stade (en fait, j'expose actuellement plusieurs implémentations "TryDequeue" juste pour jouer avec chacun d'eux). Je voulais vraiment juste discuter d'autres options qui pourraient exister.
Eric Sondergard
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.