Was ist die eleganteste Methode zum Schreiben einer "Try" -Methode in C # 7?

21

Ich schreibe eine Art Warteschlangenimplementierung mit einer TryDequeueMethode, die ein ähnliches Muster wie verschiedene .NET- TryParseMethoden verwendet. Wenn die Aktion erfolgreich war, outgebe ich einen booleschen Wert zurück und verwende einen Parameter, um den tatsächlichen Wert in der Warteschlange zurückzugeben.

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

Jetzt vermeide ich gerne outParams, wann immer ich kann. C # 7 gibt uns variable Delkarationen aus, um die Arbeit mit ihnen zu erleichtern, aber ich betrachte Params immer noch eher als notwendiges Übel als als nützliches Werkzeug.

Das von dieser Methode gewünschte Verhalten ist wie folgt:

  • Wenn es ein zu löschendes Objekt gibt, geben Sie es zurück.
  • Wenn keine zu löschenden Elemente vorhanden sind (die Warteschlange ist leer), stellen Sie dem Anrufer genügend Informationen zur Verfügung, um entsprechend zu handeln.
  • Geben Sie nicht einfach einen leeren Artikel zurück, wenn noch keine Artikel verfügbar sind.
  • Machen Sie keine Ausnahmen, wenn Sie versuchen, eine leere Warteschlange zu löschen.

Derzeit verwendet ein Aufrufer dieser Methode fast immer ein Muster wie das folgende (unter Verwendung der C # 7-Out-Variablensyntax):

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

Welches ist nicht das schlechteste, alles in allem. Aber ich kann nicht anders, als zu glauben, dass es dafür vielleicht bessere Möglichkeiten gibt (ich bin absolut bereit zuzugeben, dass es keine gibt). Ich hasse es, wenn der Anrufer seinen Fluss mit einer Bedingung unterbrechen muss, wenn er nur einen Wert erhalten möchte, falls einer existiert.

Welche anderen Muster gibt es, um dasselbe "Versuch" -Verhalten zu erreichen?

Für den Kontext kann diese Methode möglicherweise in VB-Projekten aufgerufen werden. Bonuspunkte für etwas, das in beiden Fällen gut funktioniert. Diese Tatsache sollte jedoch sehr wenig Gewicht haben.

Eric Sondergard
quelle
9
Definieren Sie eine Option<T>Struktur und geben Sie sie zurück. Diese bool Try(..., out data)Funktionen sind ein Gräuel.
CodesInChaos
2
Ich dachte ähnlich ... Vielleicht <T>, Option <T>, wenn man den OUT-Parametern widerspricht.
Jon Raynor
1
@FrustratedWithFormsDesigner Nun, ich habe noch nicht so viele andere Dinge "ausprobiert" (Wortspiele sind willkommen), wie ich an sie gedacht habe. Ich hatte die Idee, ein ValueTuple zurückzugeben, aber im besten Fall glaube ich nicht, dass dies eine große Verbesserung darstellt.
Eric Sondergard
@CodesInChaos Wir sind auf der gleichen Seite, deshalb bin ich hier! Und diese Idee gefällt mir. Wenn Sie Zeit haben, möchten Sie in einer Antwort weitere Einzelheiten angeben, damit ich sie akzeptieren kann?
Eric Sondergard

Antworten:

24

Verwenden Sie einen Optionstyp, dh ein Objekt eines Typs mit zwei Versionen, der normalerweise entweder "Some" (wenn ein Wert vorhanden ist) oder "None" (wenn kein Wert vorhanden ist) genannt wird ... oder gelegentlich Sie heißen Just and Nothing. Es gibt dann Funktionen in diesen Typen, mit denen Sie auf den Wert zugreifen können, wenn er vorhanden ist, auf Anwesenheit testen und vor allem auf eine Methode, mit der Sie eine Funktion anwenden können, die eine weitere Option auf den Wert zurückgibt, wenn er vorhanden ist (normalerweise in C #) Sie müssen FlatMap heißen, obwohl es in anderen Sprachen häufig als Bind bezeichnet wird. Der Name ist in C # von entscheidender Bedeutung, da Sie mit der Methode dieses Namens und Typs Ihre Option-Objekte in LINQ-Anweisungen verwenden können.

Zusätzliche Funktionen können Methoden wie IfPresent und IfNotPresent zum Aufrufen von Aktionen unter den relevanten Bedingungen und OrElse (das einen Standardwert ersetzt, wenn kein Wert vorhanden ist, andernfalls jedoch ein no op) usw. sein.

Ihr Beispiel könnte dann ungefähr so ​​aussehen:

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

Dies ist das Monadenmuster Option (oder Vielleicht) und es ist äußerst nützlich. Es gibt bereits Implementierungen (z. B. https://github.com/nlkl/Optional/blob/master/README.md ), aber es ist auch nicht schwer, diese selbst zu erstellen.

(Möglicherweise möchten Sie dieses Muster so erweitern, dass Sie stattdessen eine Beschreibung der Fehlerursache zurückgeben, wenn die Methode fehlschlägt. Dies ist vollständig erreichbar und wird häufig als Either-Monade bezeichnet. Wie der Name schon sagt, können Sie dieselbe FlatMap verwenden Muster, um auch in diesem Fall die Arbeit zu erleichtern)

Jules
quelle
Dies ist definitiv eine gute Option, danke für den Vorschlag und Ihre Gedanken. Ich wollte hier wirklich mehr Ideen entdecken.
Eric Sondergard
2
Das Schöne an Option/ Maybeist, dass es sich um eine Monade handelt (wenn sie natürlich als solche implementiert ist), was bedeutet, dass sie sicher verkettet, eingewickelt, ausgepackt und verarbeitet werden kann, ohne die verschiedenen Fälle jemals direkt behandeln zu müssen, und diese Verkettung und Die Verarbeitung kann mit LINQ Query Expressions erfolgen.
Jörg W Mittag
3
By the way, flatMap/ bindwird aufgerufen , SelectManyin .NET.
Jörg W Mittag
5
Ich frage mich, ob es eine bessere Idee wäre, einen anderen Namen zu wählen, da Sie auf diese Weise die Try...Namenskonventionen in .NET verletzen (und möglicherweise Betreuer verwirren).
Bob
@ Bob Das ist ein toller Punkt. Genau.
Eric Sondergard
12

Die gegebenen Antworten sind gut und ich würde mit einem von ihnen gehen. Betrachten Sie diese Antwort nur als Ergänzung einiger alternativer Ideen:

  • Machen Sie Unterklassen von Messageaufgerufenen SomeMessageund NoMessage. Dequeuekann jetzt zurückkehren, NoMessagewenn keine Nachricht vorhanden ist und SomeMessagewenn eine Nachricht vorhanden ist. Wenn der Angerufene herausfinden möchte, in welchem ​​Fall er sich befindet, kann er dies leicht durch Typprüfung tun. Wenn sie es nicht tun, machen sie NoMessageeinfach nichts, wenn eine ihrer Methoden aufgerufen wird, und hey, sie bekommen, was sie verlangt haben.

  • Das gleiche wie oben, aber Messageimplizit konvertierbar machen bool(oder implementieren operator true / operator false). Jetzt können Sie sagen if (message)und haben es wahr, wenn die Nachricht gut und falsch ist, wenn es schlecht ist. (Dies ist mit ziemlicher Sicherheit eine schlechte Idee, aber ich füge sie der Vollständigkeit halber hinzu!)

  • C # 7 hat Tupel. Gib ein (bool, Message)Tupel zurück.

  • Machen Sie Messageeinen Strukturtyp und kehren Sie zurück Message?.

Eric Lippert
quelle
Hey, danke für deine Antwort. Mit dieser Frage versuchte ich mich ebenso unterschiedlichen Ideen auszusetzen wie eine Lösung für mein spezifisches Problem zu finden. Prost!
Eric Sondergard
Ich meine, wenn Ihre "schlechte Idee" von Unity benutzt wird, warum können wir sie dann nicht benutzen?
Arturo Torres Sánchez
@ ArturoTorresSánchez: Ihre Frage lautet: "Jemand anderes hat eine wirklich schlechte Programmierpraxis angewendet. Warum kann ich das nicht?" Die Frage ist inkohärent. Niemand hindert Sie daran, die gleichen schlechten Programmierpraktiken anzuwenden wie jemand anderes. Du gehst direkt voran. Das macht es nicht zu einer guten Übung in C #.
Eric Lippert
Es war eine Zunge in der Wange. Ich denke, ich muss "/ s" verwenden, um es zu markieren.
Arturo Torres Sánchez
@ ArturoTorresSánchez: Dies ist eine Frage- und Antwortseite; Ich betrachte Fragen als Fragen , die Antworten suchen .
Eric Lippert
10

In C # 7 können Sie Pattern Matching verwenden, um dies auf elegantere Weise zu erreichen:

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

In diesem speziellen Fall würde ich wahrscheinlich Ereignisse verwenden, anstatt die Warteschlange wiederholt abzufragen.

JacquesB
quelle
3
Müssen dafür jedoch TryDequeueeinige Marker-Interfaces mit einer MessageImplementierung und einige "nichts" -Implementierungen zurückgegeben werden? Sicher, es ist eleganter auf der Call-Site, aber Sie müssen 3 Implementierungen im tatsächlichen Code implementieren, was selbst unmöglich sein kann, wenn Messagees sich um einen vorhandenen Typ handelt.
Telastyn
1
@ Telastyn: Es funktioniert auch, wenn es nur null zurückgibt . Sie können auch einen Optionstyp verwenden.
JacquesB
@JacquesB: aber was ist, wenn null ein gültiges Element in der Warteschlange ist?
JBSnorro
5

An einer Try ... -Methode (mit einem out-Parameter) ist nichts auszusetzen, wenn ein Fehler (kein Wert) genauso häufig auftritt wie ein Erfolg.

Wenn Sie jedoch darauf bestehen, die Dinge anders zu machen, möchten Sie möglicherweise eine leere Nachricht zurückgeben und diese entweder nur senden (nicht mehr Ihr Problem) oder die if-Anweisung verschieben, bis Sie wirklich wissen müssen, ob Sie eine Nachricht mit Inhalt haben oder nicht.

Beachten Sie, dass jemand den Test sowieso irgendwann durchführen muss. Ich würde sagen, der Test sollte durchgeführt werden, wann und wo die Frage aktuell ist. Dies ist zum Zeitpunkt der Dequeing.

Martin Maat
quelle
Sie machen einen guten Punkt. Sie müssen immer noch testen, egal was Sie tun. Ehrlich gesagt, kann es sein, dass ich zu diesem Zeitpunkt immer noch keine Parameter mehr habe (in der Tat lege ich derzeit mehrere "TryDequeue" -Implementierungen offen, nur um mit jedem von ihnen zu experimentieren). Ich wollte eigentlich nur andere Optionen diskutieren, die es geben könnte.
Eric Sondergard