Manchmal muss ich eine Methode oder Eigenschaft für eine Klassenbibliothek schreiben, für die es nicht ungewöhnlich ist, keine echte Antwort zu haben, sondern einen Fehler. Etwas kann nicht bestimmt werden, ist nicht verfügbar, nicht gefunden, derzeit nicht möglich oder es sind keine Daten mehr verfügbar.
Ich denke, es gibt drei mögliche Lösungen für eine solche relativ nicht außergewöhnliche Situation, um auf ein Versagen in C # 4 hinzuweisen:
- gib einen magischen Wert zurück, der sonst keine Bedeutung hat (wie
null
und-1
); - eine Ausnahme auslösen (zB
KeyNotFoundException
); - return
false
und geben Sie den tatsächlichen Rückgabewert in einemout
Parameter an (z. B.Dictionary<,>.TryGetValue
).
Die Fragen lauten also: In welcher nicht außergewöhnlichen Situation sollte ich eine Ausnahme auslösen? Und wenn ich nicht werfen sollte: Wann wird ein magischer Wert zurückgegeben, der über der Implementierung einer Try*
Methode mit einem out
Parameter angegeben ist ? (Für mich out
scheint der Parameter schmutzig zu sein, und es ist mehr Arbeit, ihn richtig zu verwenden.)
Ich suche nach sachlichen Antworten, z. B. Antworten mit Gestaltungsrichtlinien (ich kenne mich nicht mit Try*
Methoden aus), Benutzerfreundlichkeit (da ich dies für eine Klassenbibliothek verlange), Konsistenz mit der BCL und Lesbarkeit.
In der .NET Framework-Basisklassenbibliothek werden alle drei Methoden verwendet:
- gib einen magischen Wert zurück, der sonst keine Bedeutung hat:
Collection<T>.IndexOf
gibt -1 zurück,StreamReader.Read
gibt -1 zurück,Math.Sqrt
gibt NaN zurück,Hashtable.Item
gibt null zurück;
- eine Ausnahme auslösen:
Dictionary<,>.Item
wirft KeyNotFoundException,Double.Parse
löst FormatException aus; oder
- return
false
und geben Sie den tatsächlichen Rückgabewert in einemout
Parameter an:
Beachten Sie, dass es, wie Hashtable
es zu der Zeit erstellt wurde, als es in C # keine Generika gab, einen magischen Wert verwendet object
und daher zurückgeben null
kann. Bei Generika werden jedoch Ausnahmen verwendet Dictionary<,>
, und dies war anfangs nicht der Fall TryGetValue
. Anscheinend ändern sich die Einsichten.
Offensichtlich gibt es die Item
- TryGetValue
und Parse
- TryParse
Dualität aus einem Grund, daher gehe ich davon aus, dass das Auslösen von Ausnahmen für nicht außergewöhnliche Fehler in C # 4 nicht erfolgt . Die Try*
Methoden existierten jedoch nicht immer, auch wenn sie Dictionary<,>.Item
existierten.
quelle
Antworten:
Ich denke nicht, dass Ihre Beispiele wirklich gleichwertig sind. Es gibt drei verschiedene Gruppen, von denen jede ihre eigenen Gründe für ihr Verhalten hat.
StreamReader.Read
oder wenn ein einfach zu verwendender Wert vorliegt , der niemals eine gültige Antwort sein wird (-1 fürIndexOf
).Die Beispiele, die Sie angeben, sind für die Fälle 2 und 3 völlig klar. Für die magischen Werte kann argumentiert werden, ob dies eine gute Entwurfsentscheidung ist oder nicht in allen Fällen.
Der
NaN
Rückgabewert vonMath.Sqrt
ist ein Sonderfall - er folgt dem Gleitkomma-Standard.quelle
Either<TLeft, TRight>
Monade?Sie versuchen, dem Benutzer der API mitzuteilen, was er zu tun hat. Wenn Sie eine Ausnahme auslösen, müssen sie diese nicht abfangen, und nur durch Lesen der Dokumentation erfahren Sie, welche Möglichkeiten es gibt. Persönlich finde ich es langsam und mühsam, in der Dokumentation nach allen Ausnahmen zu suchen, die eine bestimmte Methode auslösen kann (selbst wenn es sich um Intellisense handelt, muss ich sie immer noch manuell herauskopieren).
Für einen magischen Wert müssen Sie weiterhin die Dokumentation lesen und möglicherweise auf eine
const
Tabelle verweisen , um den Wert zu decodieren. Zumindest hat es nicht den Overhead einer Ausnahme für das, was Sie als nicht außergewöhnliches Ereignis bezeichnen.Das ist der Grund, warum
out
ich diese Methode mit derTry...
Syntax bevorzuge, obwohl Parameter manchmal missbilligt werden . Es ist eine kanonische .NET- und C # -Syntax. Sie teilen dem Benutzer der API mit, dass er den Rückgabewert überprüfen muss, bevor er das Ergebnis verwendet. Sie können auch einen zweitenout
Parameter mit einer hilfreichen Fehlermeldung einfügen, die wiederum beim Debuggen hilft. Deshalb stimme ich für dieTry...
mitout
Parameter Lösung.Eine andere Möglichkeit ist, ein spezielles "result" -Objekt zurückzugeben, obwohl ich das viel mühsamer finde:
Dann sieht Ihre Funktion richtig aus, weil sie nur Eingabeparameter hat und nur eins zurückgibt. Es ist nur so, dass das eine, was es zurückgibt, ein Tupel ist.
In der Tat, wenn Sie in diese Art von Dingen sind, können Sie die neuen
Tuple<>
Klassen verwenden, die in .NET 4 eingeführt wurden. Persönlich mag ich die Tatsache nicht, dass die Bedeutung jedes Feldes weniger explizit ist, weil ich nicht geben kannItem1
undItem2
nützliche Namen.quelle
IMyResult
es möglich ist, ein komplexeres Ergebnis als nurtrue
oderfalse
oder den Ergebniswert zu kommunizieren .Try*()
ist nur nützlich für einfache Dinge wie die Konvertierung von String in Int.Wie Ihre Beispiele bereits zeigen, muss jeder dieser Fälle separat bewertet werden, und zwischen "außergewöhnlichen Umständen" und "Flusskontrolle" besteht ein beträchtliches Spektrum an Graustufen, insbesondere wenn Ihre Methode wiederverwendbar sein soll und möglicherweise in ganz unterschiedlichen Mustern verwendet wird als es ursprünglich vorgesehen war. Erwarten Sie nicht, dass wir uns hier alle darauf einigen, was "nicht außergewöhnlich" bedeutet, insbesondere wenn Sie sofort die Möglichkeit diskutieren, "Ausnahmen" zu verwenden, um dies umzusetzen.
Wir sind uns vielleicht auch nicht einig darüber, durch welches Design der Code am einfachsten zu lesen und zu warten ist, aber ich gehe davon aus, dass der Bibliotheksdesigner eine klare persönliche Vision davon hat und diese nur mit den anderen Überlegungen in Einklang bringen muss.
Kurze Antwort
Folgen Sie Ihrem Bauchgefühl, es sei denn, Sie entwerfen recht schnelle Methoden und erwarten die Möglichkeit einer unvorhergesehenen Wiederverwendung.
Lange Antwort
Jeder zukünftige Anrufer kann frei zwischen Fehlercodes und Ausnahmen in beide Richtungen übersetzen. Dies macht die beiden Entwurfsansätze bis auf Leistung, Debuggerfreundlichkeit und einige eingeschränkte Interoperabilitätskontexte nahezu gleich. Das läuft normalerweise auf Leistung hinaus, also konzentrieren wir uns darauf.
Als Faustregel gilt, dass das Auslösen einer Ausnahme 200x langsamer ist als eine reguläre Rückkehr (in Wirklichkeit gibt es eine signifikante Abweichung davon).
Eine andere Faustregel ist, dass das Auslösen einer Ausnahme im Vergleich zu den gröbsten magischen Werten häufig einen deutlich saubereren Code ermöglicht, da Sie sich nicht darauf verlassen müssen, dass der Programmierer den Fehlercode in einen anderen Fehlercode übersetzt, da er durch mehrere Client-Code-Ebenen in Richtung wandert Ein Punkt, an dem genügend Kontext vorhanden ist, um ihn konsistent und angemessen zu behandeln. (Sonderfall:
null
Neigt dazu, hier besser abzuschneiden als bei anderen magischen Werten, da sie dazu neigen, sichNullReferenceException
bei einigen, aber nicht allen Arten von Fehlern automatisch in einen zu übersetzen ; normalerweise, aber nicht immer, recht nahe an der Fehlerquelle. )Was ist die Lektion?
Verwenden Sie für eine Funktion, die nur einige Male während der Lebensdauer einer Anwendung aufgerufen wird (z. B. die App-Initialisierung), alles, was Sie zu einem saubereren und besser verständlichen Code führt. Leistung kann kein Problem sein.
Verwenden Sie für eine Einwegfunktion alles, was Ihnen einen saubereren Code bietet. Führen Sie dann eine Profilerstellung durch (falls erforderlich) und ändern Sie die Ausnahmen, um Codes zurückzugeben, wenn sie aufgrund von Messungen oder der gesamten Programmstruktur zu den vermuteten oberen Engpässen gehören.
Verwenden Sie für eine teure wiederverwendbare Funktion alles, was Ihnen saubereren Code bietet. Wenn Sie grundsätzlich immer einen Netzwerk-Roundtrip durchführen oder eine XML-Datei auf der Festplatte analysieren müssen, ist der Aufwand für das Auslösen einer Ausnahme wahrscheinlich vernachlässigbar. Es ist wichtiger, nicht einmal versehentlich Details eines Fehlers zu verlieren, als besonders schnell von einem "nicht außergewöhnlichen Fehler" zurückzukehren.
Eine schlanke wiederverwendbare Funktion erfordert mehr Nachdenken. Durch die Verwendung von Ausnahmen erzwingen Sie eine 100-fache Verzögerung für Anrufer, die die Ausnahme bei der Hälfte ihrer (vielen) Aufrufe sehen, wenn der Funktionskörper sehr schnell ausgeführt wird. Ausnahmen sind immer noch eine Gestaltungsoption, aber Sie müssen Anrufern, die sich dies nicht leisten können, eine Alternative mit geringem Overhead anbieten. Schauen wir uns ein Beispiel an.
Sie führen ein großartiges Beispiel dafür an
Dictionary<,>.Item
, das sich lose gesagt von der Rückgabe vonnull
Werten zum WerfenKeyNotFoundException
zwischen .NET 1.1 und .NET 2.0 geändert hat (nur, wenn Sie bereit sind, es alsHashtable.Item
praktischen, nicht generischen Vorläufer zu betrachten). Der Grund dieser "Veränderung" ist hier nicht ohne Interesse. Die Leistungsoptimierung von Werttypen (kein Boxen mehr) machte den ursprünglichen Zauberwert (null
) zu einer Nichtoption.out
Parameter würden nur einen kleinen Teil der Leistungskosten zurückbringen. Diese letztere Leistungsüberlegung ist im Vergleich zum Aufwand für das Werfen von a völlig vernachlässigbarKeyNotFoundException
, aber das Ausnahmedesign ist hier immer noch überlegen. Warum?Contains
vor jedem Aufruf den Indexer aufrufen, und dieses Muster liest sich ganz natürlich. Wenn ein Entwickler den Aufruf jedoch vergessen möchteContains
, können sich keine Leistungsprobleme einschleichen.KeyNotFoundException
ist laut genug, um bemerkt und repariert zu werden.quelle
Contains
vor einem Aufruf eines Indexers kann zu einer Race-Bedingung führen, die nicht vorliegen mussTryGetValue
.ConcurrentDictionary
für einen Multithread-Zugriff undDictionary
für einen Single-Thread-Zugriff für eine optimale Leistung verwenden. Das heißt, wenn `` Contains` nicht verwendet wird, ist der Code-Thread für diese bestimmteDictionary
Klasse NICHT sicher .Sie sollten keinen Fehler zulassen.
Ich weiß, es ist handgewellt und idealistisch, aber hör mir zu. Beim Entwerfen gibt es eine Reihe von Fällen, in denen Sie die Möglichkeit haben, eine Version ohne Fehlermodi zu bevorzugen. Anstatt ein 'FindAll' zu haben, das fehlschlägt, verwendet LINQ eine where-Klausel, die einfach eine leere Aufzählung zurückgibt. Anstatt ein Objekt zu haben, das vor der Verwendung initialisiert werden muss, lassen Sie den Konstruktor das Objekt initialisieren (oder initialisieren, wenn das nicht initialisierte erkannt wird). Der Schlüssel ist das Entfernen des Fehlerzweigs im Consumer-Code. Dies ist das Problem, also konzentrieren Sie sich darauf.
Eine andere Strategie hierfür ist das
KeyNotFound
sscenario. In fast jeder Codebasis, an der ich seit 3.0 gearbeitet habe, gibt es so etwas wie diese Erweiterungsmethode:Hierfür gibt es keinen echten Fehlermodus.
ConcurrentDictionary
hat ein ähnlichesGetOrAdd
eingebaut.Trotzdem wird es immer Zeiten geben, in denen es einfach unvermeidbar ist. Alle drei haben ihren Platz, aber ich würde die erste Option bevorzugen. Trotz allem, was aus der Gefahr von Null gemacht wird, ist es gut bekannt und passt in viele der Szenarien "Element nicht gefunden" oder "Ergebnis ist nicht anwendbar", die den Satz "Nicht außergewöhnlicher Fehler" ausmachen. Insbesondere wenn Sie nullbare Werttypen erstellen, ist die Bedeutung von "Dies könnte fehlschlagen" im Code sehr deutlich und schwer zu vergessen / zu vermasseln.
Die zweite Option ist gut genug, wenn Ihr Benutzer etwas Dummes tut. Gibt Ihnen einen String mit dem falschen Format, versucht, das Datum auf den 42. Dezember zu setzen ... etwas, das beim Testen schnell und spektakulär in die Luft gejagt werden soll, damit fehlerhafter Code erkannt und behoben wird.
Die letzte Option ist eine, die ich zunehmend ablehne. Unsere Parameter sind umständlich und verletzen häufig einige der besten Methoden bei der Erstellung von Methoden, z. B. sich auf eine Sache zu konzentrieren und keine Nebenwirkungen zu haben. Außerdem ist der out-Parameter normalerweise nur während des Erfolgs von Bedeutung. Sie sind jedoch für bestimmte Vorgänge unerlässlich, die in der Regel durch Nebenläufigkeitsprobleme oder Leistungsaspekte eingeschränkt sind (z. B. wenn Sie keine zweite Reise in die Datenbank durchführen möchten).
Wenn der Rückgabewert und der Parameter out nicht trivial sind, wird Scott Whitlocks Vorschlag für ein Ergebnisobjekt bevorzugt (wie die
Match
Klasse von Regex ).quelle
out
Parameter sind völlig orthogonal zum Problem der Nebenwirkungen. Das Ändern einesref
Parameters ist ein Nebeneffekt, und das Ändern des Status eines Objekts, den Sie über einen Eingabeparameter übergeben, ist ein Nebeneffekt. Einout
Parameter ist jedoch nur eine umständliche Methode, um eine Funktion dazu zu bringen, mehr als einen Wert zurückzugeben. Es gibt keine Nebenwirkung, nur mehrere Rückgabewerte.Immer lieber eine Ausnahme auslösen. Es verfügt über eine einheitliche Oberfläche für alle Funktionen, die ausfallen könnten, und zeigt Fehler so laut wie möglich an - eine sehr wünschenswerte Eigenschaft.
Beachten Sie, dass
Parse
undTryParse
sind nicht wirklich die gleiche Sache abgesehen von Ausfallmodi. Die Tatsache, dassTryParse
auch der Wert zurückgegeben werden kann, ist eigentlich rechtwinklig. Stellen Sie sich zum Beispiel die Situation vor, in der Sie Eingaben validieren. Es ist Ihnen eigentlich egal, wie hoch der Wert ist, solange er gültig ist. Und es ist nichts Falsches daran, eine ArtIsValidFor(arguments)
Funktion anzubieten . Es kann jedoch niemals die primäre Betriebsart sein.quelle
Wie andere angemerkt haben, ist der magische Wert (einschließlich eines booleschen Rückgabewerts) keine besonders gute Lösung, außer als "Bereichsende" -Marker. Grund: Die Semantik ist nicht explizit, auch wenn Sie die Methoden des Objekts untersuchen. Sie müssen tatsächlich die vollständige Dokumentation für das gesamte Objekt bis zu "oh ja, wenn es -42 zurückgibt, bedeutet das bla bla bla" lesen.
Diese Lösung kann aus historischen Gründen oder aus Leistungsgründen verwendet werden, sollte jedoch ansonsten vermieden werden.
Dies hinterlässt zwei allgemeine Fälle: Prüfen oder Ausnahmen.
Hier gilt die Faustregel, dass das Programm nur dann auf Ausnahmen reagieren soll, wenn das Programm / ungewollt / gegen eine Bedingung verstößt. Das Prüfen sollte verwendet werden, um sicherzustellen, dass dies nicht geschieht. Eine Ausnahme bedeutet daher entweder, dass die entsprechende Prüfung nicht im Voraus durchgeführt wurde oder dass etwas völlig Unerwartetes passiert ist.
Beispiel:
Sie möchten eine Datei aus einem bestimmten Pfad erstellen.
Sie sollten das File-Objekt verwenden, um vorab zu prüfen, ob dieser Pfad für die Erstellung oder das Schreiben von Dateien zulässig ist.
Wenn Ihr Programm dennoch versucht, auf einen Pfad zu schreiben, der illegal oder auf andere Weise nicht beschreibbar ist, sollten Sie eine Exzaption erhalten. Dies könnte aufgrund einer Racebedingung passieren (ein anderer Benutzer hat das Verzeichnis entfernt oder es nach der Prüfung schreibgeschützt gemacht).
Die Aufgabe, einen unerwarteten Fehler (signalisiert durch eine Ausnahme) zu behandeln und zu prüfen, ob die Bedingungen für die Operation im Voraus richtig sind (Sondierung), ist normalerweise unterschiedlich strukturiert und sollte daher unterschiedliche Mechanismen verwenden.
quelle
Ich denke, das
Try
Muster ist die beste Wahl, wenn der Code nur angibt, was passiert ist. Ich hasse param und mag nullable Objekt. Ich habe folgende Klasse erstelltso kann ich folgenden code schreiben
Sieht aus wie nullbar, aber nicht nur für den Werttyp
Ein anderes Beispiel
quelle
try
catch
Wenn Sie anrufenGetXElement()
und mehrmals ausfallen, wird dies Ihre Leistung in Mitleidenschaft ziehen .public struct Nullable<T> where T : struct
der Hauptunterschied in einer Einschränkung. Übrigens ist die neueste Version hier github.com/Nelibur/Nelibur/blob/master/Source/Nelibur.Sword/…Wenn Sie an der Route "magischer Wert" interessiert sind, besteht eine weitere Möglichkeit zur Lösung darin, den Zweck der Lazy-Klasse zu überladen. Obwohl Lazy die Instantiierung aufschieben soll, hindert Sie nichts wirklich daran, wie ein Vielleicht oder eine Option zu verwenden. Zum Beispiel:
quelle