Ich glaube, ich habe das gleiche Interview mit Bruce Eckel gelesen, das Sie geführt haben - und es hat mich immer nervt. Tatsächlich wurde das Argument vom Befragten (wenn dies tatsächlich der Beitrag ist, über den Sie sprechen) Anders Hejlsberg, dem MS-Genie hinter .NET und C #, vorgebracht.
http://www.artima.com/intv/handcuffs.html
Fan, obwohl ich von Hejlsberg und seiner Arbeit bin, hat mich dieses Argument immer als Schwindel empfunden. Es läuft im Grunde auf Folgendes hinaus:
"Überprüfte Ausnahmen sind schlecht, weil Programmierer sie einfach missbrauchen, indem sie sie immer abfangen und entlassen, was dazu führt, dass Probleme ausgeblendet und ignoriert werden, die sonst dem Benutzer angezeigt würden."
Mit "dem Benutzer anderweitig präsentiert" meine ich, wenn Sie eine Laufzeitausnahme verwenden, ignoriert der faule Programmierer diese einfach (im Gegensatz zum Abfangen mit einem leeren Fangblock) und der Benutzer sieht sie.
Die Zusammenfassung der Zusammenfassung des Arguments ist , dass „Programmierer sie nicht richtig verwenden und nicht mit ihnen richtig ist schlimmer , als sie nicht mit“ .
Dieses Argument hat eine gewisse Wahrheit, und ich vermute, dass Goslings Motivation, keine Operatorüberschreibungen in Java zu setzen, auf einem ähnlichen Argument beruht - sie verwirren den Programmierer, weil sie häufig missbraucht werden.
Aber am Ende finde ich es ein falsches Argument von Hejlsberg und möglicherweise ein Post-hoc-Argument, das geschaffen wurde, um den Mangel zu erklären, und keine gut durchdachte Entscheidung.
Ich würde argumentieren, dass die Überbeanspruchung von geprüften Ausnahmen zwar eine schlechte Sache ist und dazu neigt, dass Benutzer schlampig damit umgehen, aber die ordnungsgemäße Verwendung ermöglicht es dem API-Programmierer, dem API-Client-Programmierer große Vorteile zu verschaffen.
Jetzt muss der API-Programmierer darauf achten, nicht überall geprüfte Ausnahmen auszulösen, sonst nervt er den Client-Programmierer einfach. Der sehr faule Client-Programmierer wird auf den Fang zurückgreifen, (Exception) {}
wie Hejlsberg warnt, und alle Vorteile gehen verloren und die Hölle wird folgen. Unter bestimmten Umständen gibt es jedoch keinen Ersatz für eine gut geprüfte Ausnahme.
Das klassische Beispiel ist für mich die API zum Öffnen von Dateien. Jede Programmiersprache in der Geschichte der Sprachen (zumindest auf Dateisystemen) verfügt irgendwo über eine API, mit der Sie eine Datei öffnen können. Und jeder Client-Programmierer, der diese API verwendet, weiß, dass er sich mit dem Fall befassen muss, dass die Datei, die er öffnen möchte, nicht vorhanden ist. Lassen Sie mich das umformulieren: Jeder Client-Programmierer, der diese API verwendet, sollte wissen , dass er sich mit diesem Fall befassen muss. Und da ist das Problem: Kann der API-Programmierer ihnen helfen, zu wissen, dass sie damit umgehen sollten, indem sie nur Kommentare abgeben, oder können sie tatsächlich darauf bestehen, dass der Client damit umgeht ?
In C geht die Redewendung so etwas wie
if (f = fopen("goodluckfindingthisfile")) { ... }
else { // file not found ...
wo fopen
Fehler anzeigt von 0 und C zurückkehrt (dummerweise ) können Sie 0 als boolean behandeln und ... Grundsätzlich Sie dieses Idiom lernen und du bist in Ordnung. Aber was ist, wenn Sie ein Noob sind und die Redewendung nicht gelernt haben? Dann fangen Sie natürlich an mit
f = fopen("goodluckfindingthisfile");
f.read(); // BANG!
und auf die harte Tour lernen.
Beachten Sie, dass es sich hier nur um stark typisierte Sprachen handelt: Es gibt eine klare Vorstellung davon, was eine API in einer stark typisierten Sprache ist: Es ist eine Fülle von Funktionen (Methoden), die Sie mit einem klar definierten Protokoll für jede Sprache verwenden können.
Dieses klar definierte Protokoll wird typischerweise durch eine Methodensignatur definiert. Hier erfordert fopen, dass Sie ihm eine Zeichenfolge übergeben (oder ein Zeichen * im Fall von C). Wenn Sie etwas anderes angeben, wird ein Fehler beim Kompilieren angezeigt. Sie haben das Protokoll nicht befolgt - Sie verwenden die API nicht ordnungsgemäß.
In einigen (obskuren) Sprachen ist der Rückgabetyp ebenfalls Teil des Protokolls. Wenn Sie versuchen, das Äquivalent von fopen()
in einigen Sprachen aufzurufen, ohne es einer Variablen zuzuweisen, wird auch ein Fehler beim Kompilieren angezeigt (dies ist nur mit void-Funktionen möglich).
Der Punkt, den ich versuche, ist folgender: In einer statisch typisierten Sprache ermutigt der API-Programmierer den Client, die API ordnungsgemäß zu verwenden, indem er verhindert, dass der Client-Code kompiliert wird, wenn offensichtliche Fehler auftreten.
(In einer dynamisch typisierten Sprache wie Ruby können Sie alles, z. B. ein Float, als Dateinamen übergeben - und es wird kompiliert. Warum den Benutzer mit aktivierten Ausnahmen belästigen, wenn Sie nicht einmal die Methodenargumente steuern Die hier vorgebrachten Argumente gelten nur für statisch typisierte Sprachen.)
Was ist also mit 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 diesen Fang? Hier ist die Signatur für diese API-Methode:
public FileInputStream(String name)
throws FileNotFoundException
Beachten Sie, dass dies FileNotFoundException
eine aktivierte Ausnahme ist.
Der API-Programmierer sagt Ihnen Folgendes: "Sie können diesen Konstruktor verwenden, um einen neuen FileInputStream zu erstellen, aber Sie
a) muss den Dateinamen als String übergeben
b) muss die Möglichkeit akzeptieren, dass die Datei 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" steht. Mein erster Gedanke war, dass er / sie Dinge meint, die außerhalb der Kontrolle des API- Programmierers liegen. Tatsächlich sollten geprüfte Ausnahmen bei ordnungsgemäßer Verwendung jedoch für Dinge gelten, die außerhalb der Kontrolle des Client-Programmierers und des API-Programmierers liegen. Ich denke, dies ist der Schlüssel, um geprüfte Ausnahmen nicht zu missbrauchen.
Ich denke, das Öffnen der Datei veranschaulicht den Punkt gut. Der API-Programmierer weiß, dass Sie ihm möglicherweise einen Dateinamen geben, der zum Zeitpunkt des Aufrufs der API nicht vorhanden ist, und dass er Ihnen nicht das zurückgeben kann, was Sie wollten, sondern eine Ausnahme auslösen muss. Sie wissen auch, dass dies ziemlich regelmäßig vorkommt und dass der Client-Programmierer möglicherweise erwartet, dass der Dateiname zum Zeitpunkt des Aufrufs des Aufrufs korrekt ist, dies jedoch zur Laufzeit aus Gründen, die außerhalb ihrer Kontrolle liegen, möglicherweise falsch ist.
Die API macht es also explizit: Es wird Fälle geben, in denen diese Datei zum Zeitpunkt Ihres Anrufs nicht vorhanden ist und Sie sich verdammt noch mal besser damit befassen sollten.
Dies wäre bei einem Gegenfall klarer. 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. Ich könnte also versucht sein, eine aktivierte 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 "Checked" 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 einen try / catch verwenden muss, und wofür? Werden sie dem Benutzer melden, dass die falsche Zeile gesucht wurde? Wahrscheinlich nicht - denn unabhängig von der Benutzeroberfläche, die meine Tabellenansicht umgibt, sollte der Benutzer nicht in einen Zustand geraten, in dem eine unzulässige Zeile angefordert wird. Es ist also ein Fehler des Client-Programmierers.
Der API-Programmierer kann weiterhin vorhersagen, dass der Client solche Fehler codiert, und sollte dies mit einer Laufzeitausnahme wie z IllegalArgumentException
.
Mit einer aktivierten Ausnahme getRowData
ist dies eindeutig ein Fall, der dazu führen wird, dass Hejlsbergs fauler Programmierer einfach leere Fänge hinzufügt. In diesem Fall sind die unzulässigen Zeilenwerte selbst für das Debuggen des Testers oder des Client-Entwicklers nicht offensichtlich, sondern führen zu Folgefehlern, deren Ursache schwer zu bestimmen ist. Arianne-Raketen werden nach dem Start explodieren.
Okay, hier ist das Problem: Ich sage, dass die aktivierte Ausnahme FileNotFoundException
nicht nur eine gute Sache ist, sondern ein wesentliches Werkzeug in der Toolbox des API-Programmierers, um die API auf die für den Client-Programmierer nützlichste Weise zu definieren. Dies CheckedInvalidRowNumberException
ist jedoch eine große Unannehmlichkeit, die zu einer schlechten Programmierung führt und vermieden werden sollte. Aber wie erkennt man den Unterschied?
Ich denke, es ist keine exakte Wissenschaft, und ich denke, das liegt Hejlsbergs Argumentation zugrunde und rechtfertigt es vielleicht bis zu einem gewissen Grad. Aber ich bin nicht glücklich, das Baby hier mit dem Badewasser rauszuwerfen. Erlauben Sie mir daher, hier einige Regeln zu extrahieren, um gut geprüfte Ausnahmen von schlechten zu unterscheiden:
Außerhalb der Kontrolle des Kunden oder geschlossen gegen offen:
Überprüfte Ausnahmen sollten nur verwendet werden, wenn der Fehlerfall sowohl von der API als auch vom Client-Programmierer nicht kontrolliert wird . Dies hängt damit zusammen, wie offen oder geschlossen das System ist. In einer eingeschränkten Benutzeroberfläche, in der der Client-Programmierer beispielsweise die Kontrolle über alle Schaltflächen, Tastaturbefehle usw. hat, die Zeilen zur Tabellenansicht hinzufügen und daraus löschen (ein geschlossenes System), handelt es sich um einen Client-Programmierfehler, wenn er versucht, Daten abzurufen eine nicht vorhandene Zeile. In einem dateibasierten Betriebssystem, in dem eine beliebige Anzahl von Benutzern / Anwendungen Dateien hinzufügen und löschen kann (ein offenes System), ist es denkbar, dass die vom Client angeforderte Datei ohne deren Wissen gelöscht wurde, sodass von ihnen erwartet werden sollte, dass sie sich damit befassen .
Allgegenwart:
Überprüfte Ausnahmen sollten nicht für einen API-Aufruf verwendet werden, der häufig vom Client ausgeführt wird. Mit häufig meine ich von vielen Stellen im Client-Code - nicht häufig rechtzeitig. Ein Client-Code versucht also nicht oft, dieselbe Datei zu öffnen, aber meine Tabellenansicht wird RowData
von verschiedenen Methoden überall angezeigt. Insbesondere werde ich eine Menge Code wie schreiben
if (model.getRowData().getCell(0).isEmpty())
und es wird schmerzhaft sein, jedes Mal versuchen / fangen zu müssen.
Informieren des Benutzers:
Aktivierte 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 das "und was wirst du tun, wenn es passiert?" Frage, die ich oben aufgeworfen habe. Dies bezieht sich auch auf Punkt 1. Da Sie vorhersagen können, dass etwas außerhalb Ihres Client-API-Systems dazu führen kann, dass die Datei nicht vorhanden ist, können Sie dem Benutzer vernünftigerweise davon erzählen:
"Error: could not find the file 'goodluckfindingthisfile'"
Da Ihre illegale Zeilennummer durch einen internen Fehler und ohne Verschulden des Benutzers verursacht wurde, gibt es wirklich keine nützlichen Informationen, die Sie ihnen geben können. Wenn Ihre App keine Laufzeitausnahmen auf die Konsole zulässt, werden sie wahrscheinlich eine hässliche Nachricht erhalten wie:
"Internal error occured: IllegalArgumentException in ...."
Kurz gesagt, wenn Sie nicht glauben, dass Ihr Client-Programmierer Ihre Ausnahme auf eine Weise erklären kann, die dem Benutzer hilft, sollten Sie wahrscheinlich keine aktivierte Ausnahme verwenden.
Das sind also meine Regeln. Etwas erfunden, und es wird zweifellos Ausnahmen geben (bitte helfen Sie mir, sie zu verfeinern, wenn Sie so wollen). Mein Hauptargument ist jedoch, dass es Fälle gibt, in FileNotFoundException
denen die überprüfte Ausnahme für den API-Vertrag genauso wichtig und nützlich ist wie die Parametertypen. Wir sollten also nicht darauf verzichten, nur weil es missbraucht wird.
Entschuldigung, ich wollte das nicht so lang und waffelig machen. Lassen Sie mich mit zwei Vorschlägen abschließen:
A: API-Programmierer: Verwenden Sie geprüfte Ausnahmen sparsam, um ihre Nützlichkeit zu erhalten. Verwenden Sie im Zweifelsfall eine ungeprüfte Ausnahme.
B: Client-Programmierer: Gewöhnen Sie sich an, frühzeitig in Ihrer Entwicklung eine umschlossene Ausnahme (google it) zu erstellen. JDK 1.4 und höher bieten hierfür einen Konstruktor RuntimeException
, aber Sie können auch problemlos einen eigenen erstellen. Hier ist der Konstruktor:
public RuntimeException(Throwable cause)
Gewöhnen Sie sich dann an, wann immer Sie eine aktivierte Ausnahme behandeln müssen und sich faul fühlen (oder Sie denken, der API-Programmierer hat die überprüfte Ausnahme überhaupt übereifrig verwendet), schlucken Sie die Ausnahme nicht einfach, sondern wickeln Sie sie ein und wirf es neu.
try {
overzealousAPI(thisArgumentWontWork);
}
catch (OverzealousCheckedException exception) {
throw new RuntimeException(exception);
}
Fügen Sie dies in eine der kleinen Codevorlagen Ihrer IDE ein und verwenden Sie es, wenn Sie sich faul fühlen. Auf diese Weise müssen Sie, wenn Sie die aktivierte Ausnahme wirklich behandeln müssen, zurückkehren und sie beheben, nachdem Sie das Problem zur Laufzeit gesehen haben. Denn glauben Sie mir (und Anders Hejlsberg), Sie werden nie wieder zu diesem TODO in Ihrem zurückkehren
catch (Exception e) { /* TODO deal with this at some point (yeah right) */}