Ausnahmen als Behauptungen oder als Fehler?

10

Ich bin ein professioneller C-Programmierer und ein Hobby-Obj-C-Programmierer (OS X). Vor kurzem war ich aufgrund seiner sehr umfangreichen Syntax versucht, in C ++ zu expandieren.

Bisher habe ich mich nicht viel mit Ausnahmen befasst. Objective-C hat sie, aber Apples Richtlinien sind ziemlich streng:

Wichtig Sie sollten die Verwendung von Ausnahmen für Programmier- oder unerwartete Laufzeitfehler reservieren, z. B. Zugriff auf die Sammlung außerhalb der Grenzen, Versuche, unveränderliche Objekte zu mutieren, eine ungültige Nachricht zu senden und die Verbindung zum Fensterserver zu verlieren.

C ++ scheint es vorzuziehen, Ausnahmen häufiger zu verwenden. Zum Beispiel löst das Wikipedia-Beispiel auf RAII eine Ausnahme aus, wenn eine Datei nicht geöffnet werden kann. Objective-C würde return nilmit einem Fehler von einem out-Parameter gesendet. Insbesondere scheint std :: ofstream so oder so eingestellt werden zu können.

Hier bei Programmierern habe ich mehrere Antworten gefunden, die entweder proklamieren, Ausnahmen anstelle von Fehlercodes zu verwenden oder überhaupt keine Ausnahmen zu verwenden . Ersteres scheint häufiger zu sein.

Ich habe niemanden gefunden, der eine objektive Studie für C ++ durchführt. Da Zeiger selten sind, muss ich interne Fehlerflags verwenden, wenn ich Ausnahmen vermeiden möchte. Wird es zu viel Mühe geben, damit umzugehen, oder wird es vielleicht sogar besser funktionieren als Ausnahmen? Ein Vergleich beider Fälle wäre die beste Antwort.

Edit: Obwohl nicht ganz relevant, sollte ich wahrscheinlich klären, was nilist. Technisch ist es das gleiche wie NULL, aber die Sache ist, es ist in Ordnung, eine Nachricht an zu senden nil. Sie können also so etwas tun

NSError *err = nil;
id obj = [NSFileHandle fileHandleForReadingFromURL:myurl error:&err];

[obj retain];

auch wenn der erste Anruf zurückkam nil. Und wie Sie es *objin Obj-C nie tun , besteht kein Risiko einer NULL-Zeiger-Dereferenzierung.

Per Johansson
quelle
Imho, es wäre besser, wenn Sie einem Teil des Objective C-Codes zeigen würden, wie Sie dort mit Fehlern arbeiten. Es scheint, die Leute reden über etwas anderes als Sie fragen.
Gangnus
In gewisser Weise suche ich nach einer Rechtfertigung für die Verwendung von Ausnahmen. Da ich eine Vorstellung davon habe, wie sie implementiert sind, weiß ich, wie teuer sie sein können. Aber ich denke, wenn ich ein ausreichend gutes Argument für ihre Verwendung bekommen kann, werde ich diesen Weg gehen.
Per Johansson
Es scheint, dass Sie für C ++ eher eine Begründung benötigen , um sie nicht zu verwenden . Nach Reaktionen hier.
Gangnus
2
Vielleicht, aber bisher hat niemand erklärt, warum sie besser sind (außer im Vergleich zu Fehlercodes). Ich mag das Konzept, Ausnahmen für Dinge zu verwenden, die nicht außergewöhnlich sind, nicht, aber es ist eher instinktiv als auf Fakten basierend.
Per Johansson
"Ich mag es nicht ... Ausnahmen für Dinge zu verwenden, die nicht außergewöhnlich sind" - stimmte zu.
Gangnus

Antworten:

1

C ++ scheint es vorzuziehen, Ausnahmen häufiger zu verwenden.

Ich würde in mancher Hinsicht tatsächlich weniger als Objective-C vorschlagen, da die C ++ - Standardbibliothek im Allgemeinen keine Programmiererfehler wie den grenzüberschreitenden Zugriff auf eine Direktzugriffssequenz in der am häufigsten verwendeten Entwurfsform (in operator[], dh) oder auslöst versuchen, einen ungültigen Iterator zu dereferenzieren. Die Sprache greift nicht auf den Zugriff auf ein Array außerhalb der Grenzen oder die Dereferenzierung eines Nullzeigers oder dergleichen zurück.

Wenn Programmiererfehler weitgehend aus der Ausnahmebehandlungsgleichung herausgenommen werden, wird tatsächlich eine sehr große Kategorie von Fehlern entfernt, auf die andere Sprachen häufig reagieren throwing. C ++ tendiert assertin solchen Fällen dazu (was nicht in Release- / Produktions-Builds kompiliert wird, sondern nur in Debug-Builds) oder nur ausfällt (häufig abstürzt), wahrscheinlich teilweise, weil die Sprache die Kosten für solche Laufzeitprüfungen nicht auferlegen möchte Dies wäre erforderlich, um solche Programmiererfehler zu erkennen, es sei denn, der Programmierer möchte die Kosten ausdrücklich durch Schreiben von Code bezahlen, der solche Überprüfungen selbst durchführt.

Sutter empfiehlt sogar, in solchen Fällen Ausnahmen in C ++ - Codierungsstandards zu vermeiden:

Der Hauptnachteil der Verwendung einer Ausnahme zum Melden eines Programmierfehlers besteht darin, dass das Abwickeln des Stapels nicht wirklich erfolgen soll, wenn der Debugger genau in der Zeile gestartet werden soll, in der der Verstoß festgestellt wurde, wobei der Status der Zeile intakt ist. Zusammenfassend: Es gibt Fehler, von denen Sie wissen, dass sie auftreten können (siehe Punkte 69 bis 75). Für alles andere, was nicht sollte, und es ist die Schuld des Programmierers, wenn es das tut, gibt es assert.

Diese Regel ist nicht unbedingt in Stein gemeißelt. In einigen geschäftskritischeren Fällen kann es vorzuziehen sein, beispielsweise Wrapper und einen Codierungsstandard zu verwenden, der einheitlich protokolliert, wo Programmiererfehler auftreten, und throwbei Programmiererfehlern wie dem Versuch, etwas Ungültiges zu respektieren oder außerhalb der Grenzen darauf zuzugreifen, weil In diesen Fällen kann es zu kostspielig sein, keine Wiederherstellung durchzuführen, wenn die Software eine Chance hat. Insgesamt tendiert der häufigere Gebrauch der Sprache jedoch dazu, Programmierfehlern nicht ins Auge zu sehen.

Externe Ausnahmen

Wo ich sehe, dass Ausnahmen in C ++ am häufigsten empfohlen werden (laut Standardausschuss, z. B.), handelt es sich um "externe Ausnahmen", wie in einem unerwarteten Ergebnis in einer externen Quelle außerhalb des Programms. Ein Beispiel ist, dass kein Speicher zugewiesen werden kann. Zum anderen kann eine kritische Datei nicht geöffnet werden, die für die Ausführung der Software erforderlich ist. Ein anderer kann keine Verbindung zu einem erforderlichen Server herstellen. Ein anderer ist ein Benutzer, der eine Abbruchschaltfläche blockiert, um eine Operation abzubrechen, deren allgemeiner Fallausführungspfad ohne diese externe Unterbrechung einen Erfolg erwartet. All diese Dinge liegen außerhalb der Kontrolle der unmittelbaren Software und der Programmierer, die sie geschrieben haben. Es handelt sich um unerwartete Ergebnisse aus externen Quellen, die verhindern, dass die Operation (die in meinem Buch * eigentlich als unteilbare Transaktion angesehen werden sollte) erfolgreich sein kann.

Transaktionen

Ich empfehle oft, einen tryBlock als "Transaktion" zu betrachten, da Transaktionen als Ganzes erfolgreich sein oder als Ganzes fehlschlagen sollten. Wenn wir versuchen, etwas zu tun, und es auf halbem Weg fehlschlägt, müssen alle im Programmstatus vorgenommenen Nebenwirkungen / Mutationen im Allgemeinen zurückgesetzt werden, um das System wieder in einen gültigen Zustand zu versetzen, als ob die Transaktion überhaupt nicht ausgeführt worden wäre. Ebenso wie ein RDBMS, das eine Abfrage nicht zur Hälfte verarbeitet, die Integrität der Datenbank nicht beeinträchtigen sollte. Wenn Sie den Programmstatus direkt in dieser Transaktion ändern, müssen Sie die Stummschaltung aufheben, wenn ein Fehler auftritt (und hier können Scope Guards bei RAII hilfreich sein).

Die viel einfachere Alternative besteht darin , den ursprünglichen Programmstatus nicht zu ändern . Sie können eine Kopie davon mutieren und dann, wenn dies erfolgreich ist, die Kopie gegen das Original austauschen (um sicherzustellen, dass der Austausch nicht ausgelöst werden kann). Wenn dies fehlschlägt, verwerfen Sie die Kopie. Dies gilt auch dann, wenn Sie im Allgemeinen keine Ausnahmen für die Fehlerbehandlung verwenden. Eine "Transaktions" -Mentalität ist der Schlüssel zur ordnungsgemäßen Wiederherstellung, wenn vor dem Auftreten eines Fehlers Programmstatusmutationen aufgetreten sind. Es gelingt entweder als Ganzes oder scheitert als Ganzes. Es gelingt ihm nicht auf halbem Weg, seine Mutationen vorzunehmen.

Dies ist bizarrerweise eines der am seltensten diskutierten Themen, wenn ich Programmierer sehe, die nach der richtigen Fehler- oder Ausnahmebehandlung fragen, aber es ist das schwierigste von allen, in einer Software, die den Programmstatus in vielen von ihnen direkt mutieren möchte, das Richtige zu finden seine Operationen. Reinheit und Unveränderlichkeit können hier ebenso zur Erreichung der Ausnahmesicherheit beitragen wie zur Gewindesicherheit, da eine nicht auftretende Mutation / äußere Nebenwirkung nicht rückgängig gemacht werden muss.

Performance

Ein weiterer entscheidender Faktor für die Verwendung von Ausnahmen ist die Leistung, und ich meine nicht auf eine obsessive, knifflige, kontraproduktive Weise. Viele C ++ - Compiler implementieren das sogenannte "Zero-Cost Exception Handling".

Es bietet keinen Laufzeitaufwand für eine fehlerfreie Ausführung, der sogar den der Fehlerbehandlung mit C-Rückgabewert übertrifft. Als Kompromiss hat die Weitergabe einer Ausnahme einen großen Overhead.

Nach dem, was ich darüber gelesen habe, erfordern Ihre Ausführungspfade für allgemeine Fälle keinen Overhead (nicht einmal den Overhead, der normalerweise mit der Behandlung und Weitergabe von C-Fehlercodes einhergeht), im Gegenzug dafür, dass die Kosten stark auf die außergewöhnlichen Pfade verschoben werden ( was bedeutet, throwingist jetzt teurer als je zuvor).

"Teuer" ist etwas schwer zu quantifizieren, aber für den Anfang möchten Sie wahrscheinlich nicht millionenfach in eine enge Schleife werfen. Bei dieser Art von Design wird davon ausgegangen, dass Ausnahmen nicht immer links und rechts auftreten.

Nicht-Fehler

Und dieser Leistungspunkt bringt mich zu Nicht-Fehlern, was überraschend unscharf ist, wenn wir alle möglichen anderen Sprachen betrachten. Aber ich würde angesichts des oben erwähnten kostengünstigen EH-Designs sagen, dass Sie mit ziemlicher Sicherheit nicht throwauf einen Schlüssel reagieren möchten, der nicht in einem Set gefunden wird. Denn das ist wohl nicht nur ein Fehler (die Person, die nach dem Schlüssel sucht, hat möglicherweise das Set erstellt und erwartet, nach Schlüsseln zu suchen, die nicht immer existieren), sondern es wäre in diesem Zusammenhang auch enorm teuer.

Beispielsweise möchte eine Satzkreuzungsfunktion möglicherweise zwei Sätze durchlaufen und nach Schlüsseln suchen, die sie gemeinsam haben. Wenn Sie keinen Schlüssel finden threw, werden Sie eine Schleife durchlaufen und möglicherweise in der Hälfte oder mehr der Iterationen auf Ausnahmen stoßen:

Set<int> set_intersection(const Set<int>& a, const Set<int>& b)
{
     Set<int> intersection;
     for (int key: a)
     {
          try
          {
              b.find(key);
              intersection.insert(other_key);
          }
          catch (const KeyNotFoundException&)
          {
              // Do nothing.
          }
     }
     return intersection;
}

Das obige Beispiel ist absolut lächerlich und übertrieben, aber ich habe im Produktionscode einige Leute gesehen, die aus anderen Sprachen kommen und Ausnahmen in C ++ verwenden, und ich denke, es ist eine ziemlich praktische Aussage, dass dies überhaupt keine angemessene Verwendung von Ausnahmen ist in C ++. Ein weiterer Hinweis oben ist, dass Sie feststellen werden, dass der catchBlock absolut nichts zu tun hat und nur geschrieben wurde, um solche Ausnahmen gewaltsam zu ignorieren. Dies ist normalerweise ein Hinweis (wenn auch kein Garant), dass Ausnahmen in C ++ wahrscheinlich nicht sehr angemessen verwendet werden.

Für diese Arten von Fällen ist eine Art von Rückgabewert, der auf einen Fehler hinweist (alles von der Rückkehr falsezu einem ungültigen Iterator oder nullptrwas auch immer im Kontext sinnvoll ist), normalerweise weitaus geeigneter und auch oft praktischer und produktiver als ein fehlerfreier Typ von In diesem Fall ist normalerweise kein Stapelabwicklungsprozess erforderlich, um die analoge catchSite zu erreichen .

Fragen

Ich müsste mit internen Fehlerflags arbeiten, wenn ich Ausnahmen vermeiden möchte. Wird es zu viel Mühe geben, damit umzugehen, oder wird es vielleicht sogar besser funktionieren als Ausnahmen? Ein Vergleich beider Fälle wäre die beste Antwort.

Das direkte Vermeiden von Ausnahmen in C ++ erscheint mir äußerst kontraproduktiv, es sei denn, Sie arbeiten in einem eingebetteten System oder in einem bestimmten Fall, der deren Verwendung verbietet (in diesem Fall müssten Sie auch alles tun, um alles zu vermeiden Bibliotheks- und Sprachfunktionalität, die sonst throwgerne strikt verwendet würde nothrow new).

Wenn Sie Ausnahmen aus irgendeinem Grund unbedingt vermeiden müssen (z. B. Arbeiten über C-API-Grenzen eines Moduls, dessen C-API Sie exportieren), stimmen viele möglicherweise nicht mit mir überein, aber ich würde tatsächlich vorschlagen, einen globalen Fehlerbehandler / Status wie OpenGL mit zu verwenden glGetError(). Sie können dafür sorgen, dass threadlokaler Speicher verwendet wird, um einen eindeutigen Fehlerstatus pro Thread zu erhalten.

Mein Grund dafür ist, dass ich es nicht gewohnt bin, Teams in Produktionsumgebungen gründlich auf mögliche Fehler zu prüfen, wenn Fehlercodes zurückgegeben werden. Wenn sie gründlich wären, könnten einige C-APIs bei nahezu jedem einzelnen C-API-Aufruf auf einen Fehler stoßen, und eine gründliche Überprüfung würde Folgendes erfordern:

if ((err = ApiCall(...)) != success)
{
     // Handle error
}

... mit fast jeder einzelnen Codezeile, die die API aufruft und solche Überprüfungen erfordert. Trotzdem hatte ich nicht das Glück, mit so gründlichen Teams zusammenzuarbeiten. Sie ignorieren solche Fehler oft die Hälfte, manchmal sogar die meiste Zeit. Das ist für mich der größte Reiz von Ausnahmen. Wenn wir diese API umschließen und sie throwbei Auftreten eines Fehlers einheitlich gestalten , kann die Ausnahme möglicherweise nicht ignoriert werden , und meiner Ansicht nach und meiner Erfahrung nach liegt hier die Überlegenheit der Ausnahmen.

Wenn jedoch keine Ausnahmen verwendet werden können, hat der globale Fehlerstatus pro Thread zumindest den Vorteil (ein großer im Vergleich zur Rückgabe von Fehlercodes an mich), dass er möglicherweise einen früheren Fehler etwas später als zu diesem Zeitpunkt abfängt trat in einer schlampigen Codebasis auf, anstatt sie völlig zu verpassen und uns völlig bewusst zu machen, was passiert ist. Der Fehler ist möglicherweise einige Zeilen zuvor oder in einem früheren Funktionsaufruf aufgetreten. Vorausgesetzt, die Software ist noch nicht abgestürzt, können wir uns möglicherweise rückwärts arbeiten und herausfinden, wo und warum er aufgetreten ist.

Da Zeiger selten sind, muss ich interne Fehlerflags verwenden, wenn ich Ausnahmen vermeiden möchte.

Ich würde nicht unbedingt sagen, dass Zeiger selten sind. In C ++ 11 und höher gibt es sogar Methoden, um auf die zugrunde liegenden Datenzeiger von Containern und ein neues nullptrSchlüsselwort zuzugreifen. Es wird im Allgemeinen als unklug angesehen, unformatierte Zeiger zu verwenden, um Speicher zu besitzen / zu verwalten, wenn Sie unique_ptrstattdessen etwas verwenden können, wenn man bedenkt , wie wichtig es ist, RAII-konform zu sein, wenn Ausnahmen vorliegen. Aber rohe Zeiger, die keinen Speicher besitzen / verwalten, werden nicht unbedingt als so schlecht angesehen (selbst von Leuten wie Sutter und Stroustrup) und manchmal sehr praktisch, um auf Dinge zu verweisen (zusammen mit Indizes, die auf Dinge verweisen).

Sie sind wohl nicht weniger sicher als die Standard-Container-Iteratoren (zumindest in der Version, ohne überprüfte Iteratoren), die nicht erkennen, ob Sie versuchen, sie nach ihrer Ungültigmachung zu dereferenzieren. C ++ ist immer noch unverschämt eine gefährliche Sprache, würde ich sagen, es sei denn, Ihre spezifische Verwendung möchte alles einpacken und auch nicht besitzende Rohzeiger verstecken. Mit Ausnahmen ist es fast kritisch, dass Ressourcen RAII-konform sind (was im Allgemeinen keine Laufzeitkosten verursacht), aber ansonsten wird nicht unbedingt versucht, die sicherste Sprache zu sein, um Kosten zu vermeiden, die ein Entwickler nicht explizit wünscht gegen etwas anderes tauschen. Die empfohlene Verwendung versucht nicht, Sie sozusagen vor baumelnden Zeigern und ungültigen Iteratoren zu schützen (andernfalls würden wir zur Verwendung ermutigtshared_ptrüberall, was Stroustrup vehement ablehnt). Es versucht, Sie davor zu schützen, eine Ressource nicht ordnungsgemäß freizugeben, freizugeben, zu zerstören, freizuschalten oder zu bereinigen, wenn etwas passiert throws.

Drachenenergie
quelle
14

Hier ist die Sache: Aufgrund der einzigartigen Geschichte und Flexibilität von C ++ können Sie jemanden finden, der praktisch jede Meinung zu einer Funktion verkündet, die Sie möchten. Im Allgemeinen ist die Idee jedoch umso schlimmer, je mehr das, was Sie tun, wie C aussieht.

Einer der Gründe, warum C ++ in Bezug auf Ausnahmen viel lockerer ist, ist, dass Sie nicht genau return nilwissen können, wann immer Sie Lust dazu haben. nilIn den allermeisten Fällen und Typen gibt es keine .

Aber hier ist die einfache Tatsache. Ausnahmen erledigen ihre Arbeit automatisch. Wenn Sie eine Ausnahme auslösen, übernimmt RAII und alles wird vom Compiler verwaltet. Wenn Sie Fehlercodes verwenden, müssen Sie daran denken, diese zu überprüfen. Dies macht Ausnahmen von Natur aus wesentlich sicherer als Fehlercodes - sie überprüfen sich sozusagen selbst. Darüber hinaus sind sie ausdrucksvoller. Wenn Sie eine Ausnahme auslösen, erhalten Sie eine Zeichenfolge, die Sie über den Fehler informiert, und können sogar bestimmte Informationen enthalten, z. B. "Fehlerhafter Parameter X mit dem Wert Y anstelle von Z". Holen Sie sich einen Fehlercode von 0xDEADBEEF und was genau ist schief gelaufen? Ich hoffe sehr, dass die Dokumentation vollständig und aktuell ist, und selbst wenn Sie "Bad Parameter" erhalten, wird es Ihnen nie sagen, welcheParameter, welchen Wert es hatte und welchen Wert es haben sollte. Wenn Sie auch als Referenz fangen, können sie polymorph sein. Schließlich können Ausnahmen von Stellen ausgelöst werden, an denen Fehlercodes niemals möglich sind, wie z. B. Konstruktoren. Und wie wäre es mit generischen Algorithmen? Wie wird std::for_eachmit Ihrem Fehlercode umgegangen? Pro-Tipp: Ist es nicht.

Ausnahmen sind Fehlercodes in jeder Hinsicht weit überlegen. Die eigentliche Frage sind Ausnahmen und Behauptungen.

Hier ist das Ding. Niemand kann im Voraus wissen, welche Voraussetzungen Ihr Programm für den Betrieb hat, welche ungewöhnlichen Fehlerbedingungen vorliegen, welche im Voraus überprüft werden können und welche Fehler vorliegen. Dies bedeutet im Allgemeinen, dass Sie nicht im Voraus entscheiden können, ob ein bestimmter Fehler eine Behauptung oder eine Ausnahme sein soll, ohne die Programmlogik zu kennen. Darüber hinaus ist eine Operation, die fortgesetzt werden kann, wenn eine ihrer Unteroperationen fehlschlägt, die Ausnahme und nicht die Regel.

Meiner Meinung nach gibt es Ausnahmen zu fangen. Nicht unbedingt sofort, aber irgendwann. Eine Ausnahme ist ein Problem, von dem Sie erwarten, dass das Programm irgendwann wiederhergestellt werden kann. Die betreffende Operation kann sich jedoch niemals von einem Problem erholen, das eine Ausnahme rechtfertigt.

Assertionsfehler sind immer schwerwiegende, nicht behebbare Fehler. Gedächtnisbeschädigung, so etwas.

Wenn Sie eine Datei nicht öffnen können, handelt es sich dann um eine Behauptung oder Ausnahme? Nun, in einer generischen Bibliothek gibt es viele Szenarien, in denen der Fehler behandelt werden kann , z. B. beim Laden einer Konfigurationsdatei. Sie können stattdessen einfach einen vorgefertigten Standard verwenden, also würde ich Ausnahme sagen.

Als Fußnote möchte ich erwähnen, dass es ein "Null Object Pattern" -Ding gibt. Dieses Muster ist schrecklich . In zehn Jahren wird es der nächste Singleton sein. Die Anzahl der Fälle, in denen Sie ein geeignetes Nullobjekt erstellen können, ist winzig .

DeadMG
quelle
Die Alternative ist nicht unbedingt ein einfaches int. Ich würde eher etwas verwenden, das NSError ähnelt, wie ich es von Obj-C gewohnt bin.
Per Johansson
Er vergleicht nicht mit C, sondern mit Ziel C. Es ist ein großer Unterschied. Und seine Fehlercodes erklären Zeilen. Alles was du sagst ist richtig, nur irgendwie keine Antworten auf diese Frage. Keine Beleidigung gemeint.
Gangnus
Jeder, der Objective-C verwendet, wird Ihnen natürlich sagen, dass Sie absolut, völlig falsch liegen.
Gnasher729
5

Ausnahmen wurden aus einem Grund erfunden, um zu vermeiden, dass Ihr gesamter Code so aussieht:

bool success = function1(&result1, &err);
if (!success) {
    error_handler(err);
    return;
}

success = function2(&result2, &err);
if (!success) {
    error_handler(err);
    return;
}

Stattdessen erhalten Sie etwas, das eher wie folgt aussieht, mit einem Ausnahmebehandler ganz oben mainoder auf andere Weise günstig gelegen:

result1 = function1();
result2 = function2();

Einige Leute behaupten Leistungsvorteile gegenüber dem No-Exception-Ansatz, aber meiner Meinung nach überwiegen die Lesbarkeitsbedenken geringfügige Leistungssteigerungen, insbesondere wenn Sie die Ausführungszeit für alle if (!success)Boilerplates berücksichtigen, die Sie überall verteilen müssen, oder das Risiko, dass Segfaults schwerer zu debuggen sind, wenn Sie dies nicht tun. Es ist relativ selten, wenn man bedenkt, dass Ausnahmen wahrscheinlich sind.

Ich weiß nicht, warum Apple von der Verwendung von Ausnahmen abrät. Wenn sie versuchen, die Weitergabe nicht behandelter Ausnahmen zu vermeiden, erreichen Sie nur Leute, die stattdessen Nullzeiger verwenden, um Ausnahmen anzuzeigen. Ein Programmiererfehler führt also zu einer Nullzeigerausnahme anstelle einer viel nützlicheren Datei, bei der keine Ausnahme gefunden wurde, oder was auch immer.

Karl Bielefeldt
quelle
1
Dies wäre sinnvoller, wenn Code ohne Ausnahmen tatsächlich so aussieht, aber normalerweise nicht. Dieses Muster wird angezeigt, wenn error_handler nie zurückkehrt, aber selten anders.
Per Johansson
1

In dem Beitrag, auf den Sie verweisen (Ausnahmen vs. Fehlercodes), wird meiner Meinung nach eine subtil andere Diskussion geführt. Die Frage scheint zu sein, ob Sie eine globale Liste von #define-Fehlercodes haben, wahrscheinlich komplett mit Namen wie ERR001_FILE_NOT_WRITEABLE (oder abgekürzt, wenn Sie Pech haben). Und ich denke, der Hauptpunkt in diesem Thread ist, dass ein solches prozedurales Konstrukt nicht erforderlich ist, wenn Sie in einer polymporphischen Sprache unter Verwendung von Objektinstanzen programmieren. Ausnahmen können ausdrücken, welcher Fehler einfach aufgrund ihres Typs auftritt, und sie können Informationen wie die auszudruckende Nachricht (und andere Dinge) zusammenfassen. Ich würde dieses Gespräch also als ein Gespräch darüber betrachten, ob Sie prozedural in einer objektorientierten Sprache programmieren sollten oder nicht.

Die Konversation darüber, wann und wie stark man sich bei der Behandlung von Situationen, die im Code auftreten, auf Ausnahmen verlassen muss, ist eine andere. Wenn Ausnahmen ausgelöst und abgefangen werden, führen Sie ein völlig anderes Kontrollflussparadigma ein als beim herkömmlichen Aufrufstapel. Das Auslösen von Ausnahmen ist im Grunde eine goto-Anweisung, die Sie aus Ihrem Aufrufstapel herausreißt und Sie an einen unbestimmten Ort sendet (wo immer Ihr Client-Code entscheidet, Ihre Ausnahme zu behandeln). Dies macht es sehr schwierig, über Ausnahmelogik nachzudenken .

Und so gibt es ein Gefühl wie das von Jheriko, dass diese minimiert werden sollten. Angenommen, Sie lesen eine Datei, die möglicherweise auf der Festplatte vorhanden ist oder nicht. Jheriko und Apple und diejenigen, die denken, wie sie es tun (ich selbst eingeschlossen), würden argumentieren, dass das Auslösen einer Ausnahme nicht angemessen ist - das Fehlen dieser Datei ist nicht außergewöhnlich , wird erwartet. Ausnahmen sollten den normalen Kontrollfluss nicht ersetzen. Es ist genauso einfach, wenn Ihre ReadFile () -Methode beispielsweise einen Booleschen Wert zurückgibt und der Clientcode anhand des Rückgabewerts false erkennt, dass die Datei nicht gefunden wurde. Der Client-Code kann dann mitteilen, dass die Benutzerdatei nicht gefunden wurde, oder diesen Fall leise behandeln oder was auch immer er tun möchte.

Wenn Sie eine Ausnahme auslösen, belasten Sie Ihre Kunden. Sie geben ihnen Code, der sie manchmal aus dem normalen Aufrufstapel herausreißt und sie zwingt, zusätzlichen Code zu schreiben, um zu verhindern, dass ihre Anwendung abstürzt. Eine solch mächtige und erschütternde Belastung sollte ihnen nur dann aufgezwungen werden, wenn dies zur Laufzeit oder im Interesse der Philosophie "früh scheitern" unbedingt erforderlich ist. Im ersteren Fall können Sie also Ausnahmen auslösen, wenn der Zweck Ihrer Anwendung darin besteht, einen Netzwerkbetrieb zu überwachen und jemand das Netzwerkkabel abzieht (Sie signalisieren einen katastrophalen Fehler). Im letzteren Fall können Sie eine Ausnahme auslösen, wenn Ihre Methode einen Parameter ungleich Null erwartet und Sie null erhalten (Sie signalisieren, dass Sie unangemessen verwendet werden).

Was in der objektorientierten Welt anstelle von Ausnahmen zu tun ist, haben Sie Optionen, die über das Konstrukt des prozeduralen Fehlercodes hinausgehen. Eines, das sofort in den Sinn kommt, ist, dass Sie Objekte namens OperationResult oder ähnliches erstellen können. Eine Methode kann ein OperationResult zurückgeben, und dieses Objekt kann Informationen darüber enthalten, ob die Operation erfolgreich war oder nicht, und über alle anderen Informationen, die Sie haben möchten (z. B. eine Statusmeldung, aber auch subtiler Strategien zur Fehlerbehebung enthalten) ). Wenn Sie dies zurückgeben, anstatt eine Ausnahme auszulösen, wird Polymorphismus ermöglicht, der Kontrollfluss wird beibehalten und das Debuggen wird viel einfacher.

Erik Dietrich
quelle
Ich stimme Ihnen zu, aber es ist der Grund, warum mich die meisten nicht dazu gebracht haben, die Frage zu stellen.
Per Johansson
0

Es gibt ein paar schöne Kapitel in Clean Code, die genau dieses Thema behandeln - abzüglich der Apple-Sichtweise.

Die Grundidee ist, dass Sie niemals Null von Ihren Funktionen zurückgeben, weil es:

  • Fügt viel duplizierten Code hinzu
  • Macht Ihren Code durcheinander - dh es wird schwieriger zu lesen
  • Kann häufig zu Nullzeigerfehlern führen - oder zu Zugriffsverletzungen, abhängig von Ihrer speziellen Programmier- "Religion".

Die andere begleitende Idee ist, dass Sie Ausnahmen verwenden, da dies dazu beiträgt, die Unordnung in Ihrem Code weiter zu verringern, und nicht eine Reihe von Fehlercodes erstellen muss, deren Verwaltung und Wartung (insbesondere über mehrere Module und Plattformen hinweg) und später schwierig zu verfolgen ist Wenn Ihre Kunden nur schwer entschlüsseln können, erstellen Sie stattdessen aussagekräftige Nachrichten, die die genaue Art des Problems identifizieren, das Debuggen vereinfachen und Ihren Fehlerbehandlungscode auf eine einfache try..catchAnweisung reduzieren können .

In meinen COM-Entwicklungstagen hatte ich ein Projekt, in dem jeder Fehler eine Ausnahme auslöste und jede Ausnahme eine eindeutige Fehlercode-ID verwenden musste, die auf der Erweiterung der Windows-Standard-COM-Ausnahmen basiert. Es war ein Überbleibsel aus einem früheren Projekt, in dem zuvor Fehlercodes verwendet worden waren, aber das Unternehmen wollte alles objektorientiert machen und auf den COM-Zug springen, ohne die Art und Weise zu ändern, wie sie die Dinge zu sehr taten. Es dauerte nur 4 Monate, bis ihnen die Fehlercode-Nummern ausgegangen waren, und es lag an mir, große Codeteile zu überarbeiten, um stattdessen Ausnahmen zu verwenden. Sparen Sie sich besser den Aufwand und setzen Sie Ausnahmen mit Bedacht ein.

S.Robins
quelle
Danke, aber ich denke nicht darüber nach, NULL zurückzugeben. Scheint angesichts der Konstrukteure sowieso nicht möglich zu sein. Wie ich bereits erwähnt habe, müsste ich wahrscheinlich ein Fehlerflag im Objekt verwenden.
Per Johansson