Ich glaube, ich habe das gleiche Interview mit Bruce Eckel gelesen wie Sie - und es hat mich immer gestört. In der Tat wurde das Argument von dem Interviewten (wenn dies tatsächlich der Beitrag ist, von dem Sie sprechen) Anders Hejlsberg, dem MS-Genie hinter .NET und C#, vorgebracht.
http://www.artima.com/intv/handcuffs.html
Obwohl ich ein Fan von Hejlsberg und seiner Arbeit bin, habe ich dieses Argument immer als falsch empfunden. Es läuft im Grunde auf Folgendes hinaus:
"Geprüfte Ausnahmen sind schlecht, weil Programmierer sie einfach missbrauchen, indem sie sie immer abfangen und verwerfen, was dazu führt, dass Probleme versteckt und ignoriert werden, die dem Benutzer sonst angezeigt würden".
Unter "anderweitig dem Nutzer präsentiert" Ich meine, wenn Sie eine Laufzeitausnahme verwenden, wird der faule Programmierer sie einfach ignorieren (im Gegensatz zum Abfangen mit einem leeren Catch-Block) und der Benutzer wird sie sehen.
Die Zusammenfassung des Arguments ist, dass "Die Programmierer werden sie nicht richtig verwenden, und sie nicht richtig zu verwenden ist schlimmer als sie nicht zu haben. .
An diesem Argument ist etwas Wahres dran, und tatsächlich vermute ich, dass Goslings Motivation, keine Operatorüberschreibungen in Java einzuführen, auf einem ähnlichen Argument beruht - sie verwirren den Programmierer, weil sie oft missbraucht werden.
Letztendlich halte ich dies jedoch für ein Scheinargument von Hejlsberg und möglicherweise für eine nachträgliche Erklärung des Mangels und nicht für eine gut durchdachte Entscheidung.
Ich würde argumentieren, dass die übermäßige Verwendung von geprüften Ausnahmen zwar eine schlechte Sache ist und zu schlampiger Handhabung durch die Benutzer führt, dass aber die richtige Verwendung dieser Ausnahmen es dem API-Programmierer ermöglicht, dem API-Client-Programmierer große Vorteile zu bieten.
Jetzt muss der API-Programmierer darauf achten, dass er nicht überall geprüfte Ausnahmen auslöst, da diese sonst den Client-Programmierer nur verärgern. Der sehr faule Client-Programmierer wird auf catch (Exception) {}
wie Hejlsberg warnt, werden alle Vorteile verloren gehen und die Hölle wird kommen. Aber unter bestimmten Umständen gibt es einfach keinen Ersatz für eine gut geprüfte Ausnahme.
Für mich ist das klassische Beispiel die API zum Öffnen von Dateien. Jede Programmiersprache in der Geschichte der Sprachen (zumindest auf Dateisystemen) hat irgendwo eine API, mit der man eine Datei öffnen kann. Und jeder Client-Programmierer, der diese API verwendet, weiß, dass er mit dem Fall umgehen muss, dass die Datei, die er zu öffnen versucht, nicht existiert. Lassen Sie es mich anders ausdrücken: Jeder Client-Programmierer, der diese API verwendet sollten wissen dass sie sich mit diesem Fall befassen müssen. Und genau da liegt der Knackpunkt: Kann der API-Programmierer ihnen helfen, indem er ihnen mitteilt, dass sie sich allein durch Kommentare damit befassen sollten, oder können sie tatsächlich darauf bestehen der Kunde damit umgehen.
In C lautet die Redewendung etwa so
if (f = fopen("goodluckfindingthisfile")) { ... }
else { // file not found ...
où fopen
zeigt einen Fehlschlag an, indem es 0 zurückgibt, und C lässt Sie (dummerweise) 0 als einen booleschen Wert behandeln und... Im Grunde lernt man dieses Idiom und alles ist gut. Aber was ist, wenn Sie ein Anfänger sind und das Idiom nicht gelernt haben. Dann fängt man natürlich an mit
f = fopen("goodluckfindingthisfile");
f.read(); // BANG!
und lernen auf die harte Tour.
Beachten Sie, dass wir hier nur über stark typisierte Sprachen sprechen: Es gibt eine klare Vorstellung davon, was eine API in einer stark typisierten Sprache ist: Es ist ein Sammelsurium von Funktionen (Methoden), die Sie mit einem klar definierten Protokoll für jede einzelne verwenden können.
Dieses klar definierte Protokoll wird in der Regel durch eine Methodensignatur festgelegt. Hier verlangt fopen die Übergabe einer Zeichenkette (oder eines char* im Falle von C). Wenn Sie ihm etwas anderes übergeben, erhalten Sie einen Kompilierfehler. Sie haben das Protokoll nicht befolgt - Sie verwenden die API nicht richtig.
In einigen (obskuren) Sprachen ist der Rückgabetyp ebenfalls Teil des Protokolls. Wenn Sie versuchen, das Äquivalent von fopen()
in einigen Sprachen, ohne sie einer Variablen zuzuweisen, erhalten Sie ebenfalls einen Kompilierfehler (das ist nur bei ungültigen Funktionen möglich).
Was ich damit sagen will, ist Folgendes: In einer statisch typisierten Sprache ermutigt der API-Programmierer den Client, die API richtig zu verwenden, indem er verhindert, dass sein Client-Code kompiliert wird, wenn er offensichtliche Fehler macht.
(In einer dynamisch typisierten Sprache wie Ruby können Sie als Dateinamen irgendetwas, z. B. einen Float, übergeben - und es wird kompiliert. Warum sollte man den Benutzer mit geprüften Ausnahmen belästigen, wenn man nicht einmal die Methodenargumente kontrollieren kann. Die hier angeführten Argumente gelten nur für statisch typisierte Sprachen).
Und was ist mit den geprüften Ausnahmen?
Hier ist eine der Java-APIs, die Sie zum Öffnen einer Datei verwenden können.
try {
f = new FileInputStream("goodluckfindingthisfile");
}
catch (FileNotFoundException e) {
// deal with it. No really, deal with it!
... // this is me dealing with it
}
Sehen Sie den Haken? Hier ist die Signatur für diese API-Methode:
public FileInputStream(String name)
throws FileNotFoundException
Beachten Sie, dass FileNotFoundException
ist eine geprüft Ausnahme.
Der API-Programmierer sagt dies zu Ihnen: "Sie können diesen Konstruktor verwenden, um einen neuen FileInputStream zu erstellen, aber Sie
a) muss geben Sie den Dateinamen als Zeichenfolge
b) muss akzeptieren, dass die Möglichkeit, dass die Datei nicht zur Laufzeit nicht gefunden wird"
Und das ist für mich der springende Punkt.
Der Schlüssel ist im Grunde das, was in der Frage als "Dinge, die außerhalb der Kontrolle des Programmierers liegen" bezeichnet wird. Mein erster Gedanke war, dass er/sie Dinge meint, die außerhalb der API Kontrolle des Programmierers. Aber eigentlich sollten geprüfte Ausnahmen, wenn sie richtig eingesetzt werden, für Dinge verwendet werden, die sich sowohl der Kontrolle des Client-Programmierers als auch der des API-Programmierers entziehen. Ich denke, dies ist der Schlüssel dazu, geprüfte Ausnahmen nicht zu missbrauchen.
Ich denke, die geöffnete Datei veranschaulicht den Punkt sehr gut. Der API-Programmierer weiß, dass Sie ihm einen Dateinamen geben könnten, der sich zum Zeitpunkt des API-Aufrufs als nicht existent herausstellt, und dass er Ihnen nicht das Gewünschte zurückgeben kann, sondern eine Ausnahme auslösen muss. Sie wissen auch, dass dies ziemlich häufig vorkommt und dass der Client-Programmierer davon ausgeht, dass der Dateiname zum Zeitpunkt des Aufrufs korrekt ist, dass er aber zur Laufzeit aus Gründen, die außerhalb seiner Kontrolle liegen, falsch sein kann.
Die API macht es also explizit: Es wird Fälle geben, in denen diese Datei zu dem Zeitpunkt, zu dem Sie mich aufrufen, nicht existiert, und Sie sollten sich besser darum kümmern.
Dies wäre mit einem Gegenbeispiel deutlicher. Stellen Sie sich vor, ich schreibe eine Tabellen-API. Ich habe das Tabellenmodell irgendwo mit einer API, die diese Methode enthält:
public RowData getRowData(int row)
Als API-Programmierer weiß ich, dass es Fälle geben wird, in denen ein Client einen negativen Wert für die Zeile oder einen Zeilenwert außerhalb der Tabelle übergibt. Daher könnte ich versucht sein, eine geprüfte Ausnahme auszulösen und den Client zu zwingen, damit umzugehen:
public RowData getRowData(int row) throws CheckedInvalidRowNumberException
(Ich würde es natürlich nicht wirklich "geprüft" nennen.)
Dies ist eine schlechte Verwendung von geprüften Ausnahmen. Der Client-Code wird voll von Aufrufen zum Abrufen von Zeilendaten sein, von denen jeder ein try/catch verwenden muss, und wofür? Sollen sie dem Benutzer mitteilen, dass die falsche Zeile gesucht wurde? Wahrscheinlich nicht - denn wie auch immer die Benutzeroberfläche meiner Tabellenansicht aussieht, sie sollte den Benutzer nicht in einen Zustand geraten lassen, in dem eine illegale Zeile angefordert wird. Es handelt sich also um einen Fehler auf Seiten des Client-Programmierers.
Der API-Programmierer kann immer noch vorhersagen, dass der Client solche Fehler codieren wird und sollte sie mit einer Laufzeitausnahme wie einer IllegalArgumentException
.
Mit einer geprüften Ausnahme in getRowData
ist dies eindeutig ein Fall, der dazu führen wird, dass Hejlsbergs faule Programmierer einfach leere Fänge hinzufügen. Wenn das passiert, sind die illegalen Zeilenwerte nicht einmal für den Tester oder den Client-Entwickler beim Debuggen offensichtlich, sondern führen zu Folgefehlern, deren Ursache schwer zu ermitteln ist. Arianne-Raketen werden nach dem Start explodieren.
Okay, also hier ist das Problem: Ich sage, dass die geprüfte Ausnahme FileNotFoundException
ist nicht nur eine gute Sache, sondern ein unverzichtbares Werkzeug im Werkzeugkasten des API-Programmierers, um die API so zu definieren, dass sie für den Client-Programmierer möglichst nützlich ist. Aber die CheckedInvalidRowNumberException
ist eine große Unannehmlichkeit, die zu schlechter Programmierung führt und vermieden werden sollte. Aber wie kann man den Unterschied erkennen?
Ich denke, es ist keine exakte Wissenschaft, und ich denke, dass das Hejlsbergs Argument zugrunde liegt und vielleicht bis zu einem gewissen Grad gerechtfertigt ist. Aber ich möchte hier nicht das Kind mit dem Bade ausschütten. Erlauben Sie mir daher, hier einige Regeln aufzustellen, um gute geprüfte Ausnahmen von schlechten zu unterscheiden:
-
Außerhalb der Kontrolle des Kunden oder geschlossen vs. offen:
Geprüfte Ausnahmen sollten nur verwendet werden, wenn der Fehlerfall außerhalb der Kontrolle sowohl der API et der Client-Programmierer. Dies hat damit zu tun, wie öffnen o geschlossen das System ist. In einem Zwangsweise UI, bei dem der Client-Programmierer z. B. die Kontrolle über alle Schaltflächen, Tastaturbefehle usw. hat, die Zeilen in der Tabellenansicht hinzufügen und löschen (ein geschlossenes System), ist es ein Fehler in der Client-Programmierung, wenn versucht wird, Daten aus einer nicht vorhandenen Zeile zu holen. In einem dateibasierten Betriebssystem, in dem eine beliebige Anzahl von Benutzern/Anwendungen Dateien hinzufügen und löschen können (ein offenes System), ist es denkbar, dass die Datei, die der Client anfordert, ohne sein Wissen gelöscht wurde, so dass von ihm erwartet werden sollte, dass er sich darum kümmert.
-
Ubiquität:
Geprüfte Ausnahmen sollten nicht bei einem API-Aufruf verwendet werden, der häufig vom Client durchgeführt wird. Mit häufig meine ich von vielen Stellen im Client-Code - nicht häufig in der Zeit. So neigt ein Client-Code nicht dazu, dieselbe Datei häufig zu öffnen, aber meine Tabellensicht erhält RowData
mit verschiedenen Methoden. Insbesondere werde ich eine Menge Code schreiben wie
if (model.getRowData().getCell(0).isEmpty())
und es wird schmerzhaft sein, jedes Mal in try/catch einpacken zu müssen.
-
Information des Nutzers:
Geprüfte Ausnahmen sollten in Fällen verwendet werden, in denen Sie sich vorstellen können, dass dem Endbenutzer eine nützliche Fehlermeldung angezeigt wird. Dies ist die "Und was werden Sie tun, wenn es passiert?" Frage, die ich oben gestellt habe. Sie bezieht sich auch auf Punkt 1. Da Sie vorhersagen können, dass etwas außerhalb Ihres Client-API-Systems dazu führen könnte, dass die Datei nicht vorhanden ist, können Sie den Benutzer vernünftigerweise darüber informieren:
"Error: could not find the file 'goodluckfindingthisfile'"
Da die illegale Zeilennummer durch einen internen Fehler und ohne Verschulden des Benutzers verursacht wurde, gibt es keine nützlichen Informationen, die Sie ihm geben können. Wenn Ihre Anwendung keine Laufzeitausnahmen an die Konsole weiterleitet, wird sie wahrscheinlich eine hässliche Meldung wie "Ich habe einen Fehler gemacht" ausgeben:
"Internal error occured: IllegalArgumentException in ...."
Kurz gesagt, wenn Sie nicht glauben, dass Ihr Kundenprogrammierer Ihre Ausnahme so erklären kann, dass sie dem Benutzer hilft, dann sollten Sie wahrscheinlich keine geprüfte Ausnahme verwenden.
Das sind also meine Regeln. Sie sind etwas ausgeklügelt, und es wird zweifellos Ausnahmen geben (bitte helfen Sie mir, sie zu verfeinern, wenn Sie wollen). Aber mein Hauptargument ist, dass es Fälle gibt wie FileNotFoundException
wobei die geprüfte Ausnahme ein ebenso wichtiger und nützlicher Teil des API-Vertrags ist wie die Parametertypen. Wir sollten sie also nicht abschaffen, nur weil sie missbraucht wird.
Tut mir leid, ich wollte das nicht so lang und schwammig machen. Lassen Sie mich mit zwei Vorschlägen schließen:
A: API-Programmierer: Verwenden Sie geprüfte Ausnahmen sparsam, um ihre Nützlichkeit zu erhalten. Im Zweifelsfall verwenden Sie eine ungeprüfte Ausnahme.
B: Client-Programmierer: Machen Sie es sich zur Gewohnheit, schon früh in der Entwicklung eine Wrapped Exception zu erstellen (googeln Sie es). JDK 1.4 und später bieten einen Konstruktor in RuntimeException
dafür, aber Sie können auch leicht Ihre eigenen erstellen. Hier ist der Konstruktor:
public RuntimeException(Throwable cause)
Dann machen Sie es sich zur Gewohnheit, immer dann, wenn Sie eine geprüfte Ausnahme behandeln müssen und Sie sich faul fühlen (oder Sie denken, dass der API-Programmierer übereifrig war, die geprüfte Ausnahme überhaupt zu verwenden), die Ausnahme nicht einfach zu schlucken, sondern sie einzuwickeln und erneut zu verwerfen.
try {
overzealousAPI(thisArgumentWontWork);
}
catch (OverzealousCheckedException exception) {
throw new RuntimeException(exception);
}
Fügen Sie dies in eine der kleinen Code-Vorlagen Ihrer IDE ein und verwenden Sie es, wenn Sie faul sind. Auf diese Weise sind Sie, wenn Sie die geprüfte Ausnahme wirklich behandeln müssen, gezwungen, zurückzukommen und sich darum zu kümmern, nachdem Sie das Problem zur Laufzeit gesehen haben. Denn, glauben Sie mir (und Anders Hejlsberg), Sie werden nie wieder zu diesem TODO in Ihrem
catch (Exception e) { /* TODO deal with this at some point (yeah right) */}