it-swarm.com.de

Magischen Wert zurückgeben, Ausnahme auslösen oder bei Fehler false zurückgeben?

Manchmal muss ich eine Methode oder Eigenschaft für eine Klassenbibliothek schreiben, für die es keine Ausnahme ist, keine echte Antwort zu haben, sondern einen Fehler. Etwas kann nicht festgestellt werden, ist nicht verfügbar, nicht gefunden, derzeit nicht möglich oder es sind keine weiteren Daten 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:

  • einen magischen Wert zurückgeben, der sonst keine Bedeutung hat (wie null und -1);
  • eine Ausnahme auslösen (z. B. KeyNotFoundException);
  • geben Sie false zurück und geben Sie den tatsächlichen Rückgabewert in einem out -Parameter an (z. B. Dictionary<,>.TryGetValue ).

Die Fragen sind also: In welcher nicht außergewöhnlichen Situation sollte ich eine Ausnahme auslösen? Und wenn ich nicht auslösen sollte: Wann wird ein magischer Wert zurückgegeben, der über der Implementierung einer Try* - Methode mit einem out -Parameter liegt? (Für mich scheint der Parameter out schmutzig zu sein, und es ist mehr Arbeit, ihn richtig zu verwenden.)

Ich suche nach sachlichen Antworten, z. B. nach Antworten mit Entwurfsrichtlinien (ich weiß nichts über Try* - Methoden), Benutzerfreundlichkeit (da ich dies für eine Klassenbibliothek fordere), Konsistenz mit der BCL und Lesbarkeit .


In der .NET Framework-Basisklassenbibliothek werden alle drei Methoden verwendet:

Beachten Sie, dass Hashtable in der Zeit erstellt wurde, als es in C # keine Generika gab, object verwendet und daher null als magischen Wert zurückgeben kann. Bei Generika werden jedoch Ausnahmen in Dictionary<,> Verwendet, und anfangs hatte es kein TryGetValue. Anscheinend ändern sich die Einsichten.

Offensichtlich gibt es die Dualität Item-TryGetValue und Parse-TryParse aus einem Grund, daher gehe ich davon aus, dass es Ausnahmen für nicht außergewöhnliche Fehler gibt C # 4 nicht fertig . Die Methoden Try* Existierten jedoch nicht immer, selbst wenn Dictionary<,>.Item Existierte.

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.

  1. Der magische Wert ist eine gute Option, wenn eine "Bis" -Bedingung wie StreamReader.Read Liegt oder wenn es einen einfach zu verwendenden Wert gibt, der niemals eine gültige Antwort sein wird (-1 für IndexOf).
  2. Ausnahme auslösen, wenn die Semantik der Funktion darin besteht, dass der Aufrufer sicher ist, dass sie funktioniert. In diesem Fall ist ein nicht vorhandener Schlüssel oder ein schlechtes Doppelformat wirklich außergewöhnlich.
  3. Verwenden Sie einen out-Parameter und geben Sie einen Bool zurück, wenn die Semantik prüfen soll, ob die Operation möglich ist oder nicht.

Die von Ihnen angegebenen Beispiele sind für die Fälle 2 und 3 vollkommen klar. Für die magischen Werte kann argumentiert werden, ob dies eine gute Entwurfsentscheidung ist oder nicht in allen Fällen.

Das von Math.Sqrt Zurückgegebene NaN ist ein Sonderfall - es folgt dem Gleitkomma-Standard.

58
Anders Abel

Sie versuchen, dem Benutzer der API mitzuteilen, was er zu tun hat. Wenn Sie eine Ausnahme auslösen, werden sie durch nichts gezwungen, sie abzufangen. Wenn Sie nur die Dokumentation lesen, werden sie über alle Möglichkeiten informiert. Persönlich finde ich es langsam und mühsam, in der Dokumentation nach allen Ausnahmen zu suchen, die eine bestimmte Methode auslösen könnte (selbst wenn es sich um Intellisense handelt, muss ich sie immer noch manuell kopieren).

Für einen magischen Wert müssen Sie weiterhin die Dokumentation lesen und möglicherweise auf eine const -Tabelle verweisen, um den Wert zu dekodieren. Zumindest hat es nicht den Overhead einer Ausnahme für das, was Sie als nicht außergewöhnliches Ereignis bezeichnen.

Deshalb bevorzuge ich diese Methode mit der Syntax Try..., Obwohl out -Parameter manchmal verpönt sind. 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 zweiten out -Parameter mit einer hilfreichen Fehlermeldung einfügen, die wiederum beim Debuggen hilft. Deshalb stimme ich für die Try... Mit out Parameterlösung.

Eine andere Möglichkeit besteht darin, ein spezielles "Ergebnis" -Objekt zurückzugeben, obwohl ich dies viel mühsamer finde:

interface IMyResult
{
    bool Success { get; }
    // Only access this if Success is true
    MyOtherClass Result { get; }
    // Only access this if Success is false
    string ErrorMessage { get; }
}

Dann ist Ihre Funktion sieht richtig aus, weil sie nur Eingabeparameter hat und nur eine Sache zurückgibt. Es ist nur so, dass das einzige, was es zurückgibt, eine Art Tupel ist.

Wenn Sie sich für solche Dinge interessieren, können Sie die neuen Tuple<> - Klassen verwenden, die in .NET 4 eingeführt wurden. Ich persönlich mag die Tatsache nicht, dass die Bedeutung der einzelnen Felder weniger explizit ist weil ich Item1 und Item2 keine nützlichen Namen geben kann.

33
Scott Whitlock

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 Grauspektrum, insbesondere wenn Ihre Methode wiederverwendbar sein soll und in ganz unterschiedlichen Mustern verwendet werden kann als es ursprünglich für gedacht war. Erwarten Sie nicht, dass wir uns hier alle darüber einig sind, 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, 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 gegen die anderen Überlegungen abwägen muss.

Kurze Antwort

Folgen Sie Ihren Bauchgefühlen, es sei denn, Sie entwickeln ziemlich schnelle Methoden und erwarten die Möglichkeit einer unvorhergesehenen Wiederverwendung.

Lange Antwort

Jeder zukünftige Anrufer kann frei zwischen Fehlercodes und Ausnahmen übersetzen, wie er es in beide Richtungen wünscht. Dies macht die beiden Entwurfsansätze bis auf Leistung, Debugger-Freundlichkeit und einige eingeschränkte Interoperabilitätskontexte nahezu gleichwertig. Dies läuft normalerweise auf die Leistung hinaus, also konzentrieren wir uns darauf.

  • Als Faustregel ist zu erwarten, dass das Auslösen einer Ausnahme 200-mal langsamer ist als eine reguläre Rückgabe (in Wirklichkeit gibt es eine signifikante Abweichung davon).

  • Als Faustregel gilt, dass das Auslösen einer Ausnahme im Vergleich zu den gröbsten magischen Werten häufig einen viel saubereren Code ermöglicht, da Sie sich nicht darauf verlassen müssen, dass der Programmierer den Fehlercode in einen anderen Fehlercode übersetzt, da er mehrere Ebenen des Clientcodes durchläuft Ein Punkt, an dem es genügend Kontext gibt, um konsistent und angemessen damit umzugehen. (Sonderfall: null schneidet hier tendenziell besser ab als andere magische Werte, da es bei einigen, aber nicht allen Arten von Fehlern dazu neigt, sich automatisch in ein NullReferenceException zu übersetzen; normalerweise aber nicht immer, ganz in der Nähe der Fehlerquelle.)

Was ist die Lektion?

Verwenden Sie für eine Funktion, die während der Lebensdauer einer Anwendung nur einige Male aufgerufen wird (z. B. die App-Initialisierung), alles, was Ihnen saubereren und verständlicheren Code bietet. Leistung kann kein Problem sein.

Verwenden Sie für eine Wegwerffunktion alles, was Ihnen saubereren Code liefert. Führen Sie dann eine Profilerstellung durch (falls erforderlich) und ändern Sie Ausnahmen, um Codes zurückzugeben, wenn sie aufgrund von Messungen oder der gesamten Programmstruktur zu den vermuteten Top-Engpässen gehören.

Verwenden Sie für eine teure wiederverwendbare Funktion alles, was Ihnen saubereren Code bietet. Wenn Sie grundsätzlich immer eine Netzwerkrundfahrt 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 Überlegungen. Durch die Verwendung von Ausnahmen erzwingen Sie eine 100-fache Verlangsamung für Anrufer, die die Ausnahme bei der Hälfte ihrer (vielen) Anrufe sehen, wenn die Der Hauptteil der Funktion wird sehr schnell ausgeführt. Ausnahmen sind immer noch eine Entwurfsoption, aber Sie müssen eine Alternative mit geringem Overhead für Anrufer bereitstellen, die sich dies nicht leisten können. Schauen wir uns ein Beispiel an.

Sie listen ein großartiges Beispiel für Dictionary<,>.Item, das sich lose von der Rückgabe von null -Werten zum Auslösen von KeyNotFoundException zwischen .NET 1.1 und .NET 2.0 geändert hat (nur wenn Sie bereit sind, Hashtable.Item sein praktischer nicht generischer Vorläufer zu sein). Der Grund für diese "Änderung" ist hier nicht ohne Interesse. Die Leistungsoptimierung von Werttypen (kein Boxen mehr) machte den ursprünglichen magischen Wert (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 eines KeyNotFoundException vollständig vernachlässigbar , aber das Ausnahmedesign ist hier immer noch überlegen. Warum?

  • ref/Out-Parameter verursachen jedes Mal ihre Kosten, nicht nur im Fall eines "Fehlers"
  • Jeder, der sich darum kümmert, kann vor jedem Aufruf des Indexers Contains aufrufen, und dieses Muster liest sich ganz natürlich. Wenn ein Entwickler möchte, aber vergisst, Contains aufzurufen, können sich keine Leistungsprobleme einschleichen. KeyNotFoundException ist laut genug, um bemerkt und behoben zu werden.
17
Jirka Hanika

Was ist in einer relativ nicht außergewöhnlichen Situation am besten, um auf ein Versagen hinzuweisen, und warum?

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 eine 'FindAll' zu haben, die 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 Objekt 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 Szenario KeyNotFound. In fast jeder Codebasis, an der ich seit 3.0 gearbeitet habe, gibt es so etwas wie diese Erweiterungsmethode:

public static class DictionaryExtensions {
    public static V GetValue<K, V>(this IDictionary<K, V> arg, K key, Func<K,V> ifNotFound) {
        if (!arg.ContainsKey(key)) {
            return ifNotFound(key);
        }

        return arg[key];
    }
}

Hierfür gibt es keinen echten Fehlermodus. ConcurrentDictionary hat ein ähnliches GetOrAdd eingebaut.

Trotzdem wird es immer Zeiten geben, in denen dies einfach unvermeidlich ist. Alle drei haben ihren Platz, aber ich würde die erste Option bevorzugen. Trotz allem, was aus der Gefahr von Null besteht, ist es bekannt und passt in viele der Szenarien "Artikel nicht gefunden" oder "Ergebnis ist nicht anwendbar", aus denen sich der Satz "Nicht außergewöhnlicher Fehler" zusammensetzt. Insbesondere wenn Sie nullbare Werttypen erstellen, ist die Bedeutung von "Dies könnte fehlschlagen" im Code sehr explizit und schwer zu vergessen/zu vermasseln.

Die zweite Option ist gut genug, wenn Ihr Benutzer etwas Dummes tut. Gibt Ihnen eine Zeichenfolge mit dem falschen Format, versucht, das Datum auf den 42. Dezember einzustellen ... etwas, das beim Testen schnell und spektakulär explodieren soll, damit fehlerhafter Code identifiziert und behoben wird.

Die letzte Option ist eine, die ich zunehmend nicht mag. Unsere Parameter sind umständlich und verstoßen bei der Erstellung von Methoden gegen einige der Best Practices, z. B. wenn Sie sich auf eine Sache konzentrieren und keine Nebenwirkungen 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 normalerweise durch Parallelitätsprobleme oder Leistungsaspekte eingeschränkt sind (wenn Sie beispielsweise keine zweite Reise in die Datenbank unternehmen möchten).

Wenn der Rückgabewert und der Parameter out nicht trivial sind, wird der Vorschlag von Scott Whitlock zu einem Ergebnisobjekt bevorzugt (wie die Klasse Match von Regex).

10
Telastyn

Ziehen Sie es immer vor, eine Ausnahme auszulösen. Es hat eine einheitliche Schnittstelle zwischen allen Funktionen, die ausfallen könnten, und es zeigt einen Fehler so laut wie möglich an - eine sehr wünschenswerte Eigenschaft.

Beachten Sie, dass Parse und TryParse abgesehen von den Fehlermodi nicht wirklich dasselbe sind. Die Tatsache, dass TryParse auch den Wert zurückgeben kann, ist wirklich etwas orthogonal. Stellen Sie sich die Situation vor, in der Sie beispielsweise eine Eingabe validieren. Es ist dir eigentlich egal, was der Wert ist, solange er gültig ist. Und es ist nichts Falsches daran, eine Art IsValidFor(arguments) - Funktion anzubieten. Es kann jedoch niemals die Betriebsart primär sein.

3
DeadMG

Wie andere angemerkt haben, ist der magische Wert (einschließlich eines booleschen Rückgabewerts) keine so gute Lösung, außer als "End-of-Range" -Marker. Grund: Die Semantik ist nicht explizit, selbst wenn Sie die Methoden des Objekts untersuchen. Sie müssen tatsächlich die vollständige Dokumentation für das gesamte Objekt lesen, bis hin zu "oh ja, wenn es -42 zurückgibt, bedeutet das bla bla bla".

Diese Lösung kann aus historischen Gründen oder aus Leistungsgründen verwendet werden, sollte aber ansonsten vermieden werden.

Dies lässt zwei allgemeine Fälle übrig: Prüfung oder Ausnahmen.

Hier gilt die Faustregel, dass das Programm nur dann auf Ausnahmen reagieren sollte, wenn das Programm/unbeabsichtigt/eine Bedingung verletzt. Es sollte geprüft 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 im Voraus zu prüfen, ob dieser Pfad für die Erstellung oder das Schreiben von Dateien zulässig ist.

Wenn Ihr Programm immer noch versucht, auf einen Pfad zu schreiben, der illegal oder auf andere Weise nicht beschreibbar ist, sollten Sie eine Exaption erhalten. Dies kann aufgrund einer Racebedingung passieren (ein anderer Benutzer hat das Verzeichnis entfernt oder es schreibgeschützt, nachdem Sie Probleme haben).

Die Aufgabe, einen unerwarteten Fehler zu behandeln (durch eine Ausnahme signalisiert) und im Voraus zu prüfen, ob die Bedingungen für die Operation richtig sind (Prüfung), ist normalerweise unterschiedlich strukturiert und sollte daher unterschiedliche Mechanismen verwenden.

2
Anders Johansen

Wenn Sie an der "magischen Wert" -Route interessiert sind, besteht eine weitere Möglichkeit, dies zu lösen, darin, den Zweck der Lazy-Klasse zu überladen. Obwohl Lazy die Instanziierung verschieben soll, hindert Sie nichts wirklich daran, wie ein Vielleicht oder eine Option zu verwenden. Zum Beispiel:

    public static Lazy<TValue> GetValue<TValue, TKey>(
        this IDictionary<TKey, TValue> dictionary,
        TKey key)
    {
        TValue retVal;
        if (dictionary.TryGetValue(key, out retVal))
        {
            var retValRef = retVal;
            var lazy = new Lazy<TValue>(() => retValRef);
            retVal = lazy.Value;
            return lazy;
        }

        return new Lazy<TValue>(() => default(TValue));
    }
0
zumalifeguard

Ich denke, das Try Muster ist die beste Wahl, wenn der Code nur angibt, was passiert ist. Ich hasse Parameter und mag nullbare Objekte. Ich habe folgende Klasse erstellt

public sealed class Bag<TValue>
{
    public Bag(TValue value, bool hasValue = true)
    {
        HasValue = hasValue;
        Value = value;
    }

    public static Bag<TValue> Empty
    {
        get { return new Bag<TValue>(default(TValue), false); }
    }

    public bool HasValue { get; private set; }
    public TValue Value { get; private set; }
}

so kann ich folgenden Code schreiben

    public static Bag<XElement> GetXElement(this XElement element, string elementName)
    {
        try
        {
            XElement result = element.Element(elementName);
            return result == null
                       ? Bag<XElement>.Empty
                       : new Bag<XElement>(result);
        }
        catch (Exception)
        {
            return Bag<XElement>.Empty;
        }
    }

Sieht aus wie nullbar, aber nicht nur für den Werttyp

Ein anderes Beispiel

    public static Bag<string> TryParseString(this XElement element, string attributeName)
    {
        Bag<string> attributeResult = GetString(element, attributeName);
        if (attributeResult.HasValue)
        {
            return new Bag<string>(attributeResult.Value);
        }
        return Bag<string>.Empty;
    }

    private static Bag<string> GetString(XElement element, string attributeName)
    {
        try
        {
            string result = element.GetAttribute(attributeName).Value;
            return new Bag<string>(result);
        }
        catch (Exception)
        {
            return Bag<string>.Empty;
        }
    }
0
GSerjo