Dieser Artikel soll Exceptions und Asserts beleuchten, wozu man sie braucht, wann man sie einsetzt und welche Fehler mal vermeiden sollte.
Exceptions
Exceptions sind Ausnahmebedingungen im Programm. In vielen Sprachen gibt es Exceptions, in C++, C# und VB.NET (bzw. alle .NET-Sprachen), Javascript und sogar in PHP ab Version 5. In Visual C unter Win32 gibt es SEH. Eine Exception wird mit einem throw-Schlüsselwort geworfen. Es kann für gewöhnlich ein Objekt jedes beliebigen Typs geworfen werden, wobei viele Sprachen Exception-Klassen und ganze -Hierarchien definieren.
Programmcode, der Exceptions auslösen kann, kann mit einem try-Block umschlossen werden. Aufgefangen wird eine Exception dann im nachfolgenden catch-Block, und man kann meist mehrere Blöcke für verschiedene Typen angeben. Verläßt der Programmfluß den catch-Teil, ohne wieder eine Exception auszulösen, gilt die ursprüngliche Ausnahme als gehandlet. Es kann die aufgefangene meist rethrow’t werden, wenn das benötigt wird. Manche Sprachen erlauben einen finally-Block, der nach der Exception-Behandlung oder dem try-Block ausgeführt wird. C++ braucht das nicht, und Bjarne Stroustrup erklärt, warum.
Wird eine Exception vom Programm nicht gehandlet, wird das Programm beendet, und zwar ohne weitere Meldung. Stellte die Applikation ein Fenster da, scheint das Programm von einem auf den nächsten Moment verschwunden zu sein. Dies gilt es natürlich zu vermeiden. In C++ gibt es eine weitere Verhaltensweise, die ähnlich arbeitet. Wird eine Exception geworfen, während das “stack unwinding” passiert, also z.B. in einem Destruktor, beendet sich das Programm auch (siehe z.B. dieser Artikel von Boris Kolpackov). Selbes passiert, wenn eine Funktion oder Methode eine Exception wirft, die sie nicht in ihrer “exception specification”-Liste erwähnt. Also nicht so ganz trivial in C++.
Nicht ganz einfach ist auch die Frage, wo und wann Exceptions geworfen werden sollen. Dies wird z.B. in einem Artikel von Ned Batchelder aufgegriffen:
http://nedbatchelder.com/text/exceptions-in-the-rainforest.html
Asserts
Asserts ist bestimmt auch schon jeder begegnet. In C++ über den Header <cassert> bereitgestellt kann man die assert()-Methode aufrufen. Sie zeigt z.B. unter Win32 eine MessageBox an, wenn der Ausdruck, die der Funktion mitgegeben wird, gleich 0 oder false ist. “to assert” heißt übersetzt “bestätigen” oder “sich versichern”, und genau dafür wird die Funktion benutzt. In der MFC gibts das Makro ASSERT (und VALIDATE), in ATL heißt es ATLASSERT (bzw. ATLVALIDATE), in der C-Runtime gibts zusätzlich _ASSERT und _ASSERTE, in .NET-Sprachen gibts System.Diagnostics.Debug.Assert, und auch in PHP gibt es eine entsprechende Funktion.
Besonderheit der von Microsoft definierten assert-Makros wie ASSERT und ATLASSERT ist, daß sie über die defines _DEBUG und NDEBUG gesteuert werden. Ist NDEBUG definiert, z.B. beim Kompilieren für das Release-Target, ist ASSERT ein leeres Statement. Mögliche Fehlerquelle ist, wenn man ein Funktionsaufruf in ASSERT verwendet. Dafür hat Microsoft zusätzlich VERIFY erfunden. Die Makros sind im Release-Modus also no-op’s, im Debug-Modus zeigen sie die oft verwünschten Dialogboxen an. In .NET ist es ähnlich, dort wird allerdings im Konfigurationsfile für die Applikation angegeben, ob die System.Diagnostics.Debug-Klasse etwas ausgibt.
Wer Unit-Tests mit den verschiedenen verfügbaren Frameworks (wie CppUnit, NUnit, PHPUnit, etc.) schreibt, ist noch ein anderer Assert untergekommen. Mit CPPUNIT_ASSERT(), NUnit.Assert, etc. wird z.B. in einer Testfunktion ein erwarteter Zustand abgefragt. Da man Unit-Tests auch mit einer Release-Version laufen lassen will, um sicherzustellen, daß es auch dort keine Überraschungen gibt, müssen diese Asserts natürlich immer ausgeführt werden. Sie heißen zwar auch so, sind aber für was anderes gedacht.
Exceptions vs. Asserts
Wann aber verwendet man was? Manche schwören auf Exceptions, andere benutzen (noch) Rückgabewerte zur Fehlerbehandlung. Sie sagen, Exceptions sind unkontrollierbar und setzen sich durchs ganze Programm fort, wenn man nicht aufpaßt. Genauso die Assert-Skeptiker, die bei der Erwähnung der selben schon überall MessageBoxen aufgehen sehen. Beide haben aber Vorteile und sollten so eingesetzt werden, wie die beiden Features angedacht sind. Und das geht sogar gleichzeitig.
Wenn man sich sonst nichts von diesem Post merken braucht, sind es die zwei Kriterien für den Einsatz von Exceptions und Asserts:
- Exceptions werden verwendet, wenn ein vorhersehbarer Fehler eintritt und gehandled werden soll
- Asserts sind zu verwenden, um unvorhersagbare Fehler während der Entwicklung festgestellt werden sollen
Exceptions können z.B. auftreten, wenn eine Datei, die ich an einer bestimmten Stelle erwarte, nicht vorhanden ist. Oder wenn ein Soap-Aufruf ein Datenfeld zurückliefert, das Daten in einem anderen Format beinhaltet, als das Programm es erwartet (Soap-Aufrufe können ja von jedermann generiert werden). Exceptions decken also Fehler auf, mit denen gerechnet werden muß.
Exceptions sind im ausgelieferten Programm immer noch vorhanden, müssen also gehandled werden. Irgendwann muß eine lesbare Meldung formatiert und dem Benutzer präsentiert werden. Möglicherweise kann er das Problem beheben (Datei an die passende Stelle kopieren), möglicherweise kann er aber auch nicht viel machen und muß das Programm beenden.
Vorteil von Exceptions ist, daß man sie nicht ignorieren kann. Solche vorhersehbaren Fehler dürfen auch nicht ignoriert werden. Die Ausnahme wird sonst an übergeordnete Programmteile weitergegeben. Das “Vergessen” des Prüfens eines Return-Wertes gibt es damit nicht mehr. Nachteil ist, daß Exception-Handling Disziplin vom Programmierer erfordert. Es muß jede Exception aufgefangen und in irgend einer Weise behandelt werden. Es muß schon vor dem Schreiben von Programmcode klar sein, in welcher Schicht Exceptions gehandled werden, und wo sie nur durchgelassen werden. Es muß festgelegt sein, welche Exception-Klassen verwendet werden. Daraus resultiert im Ende dann eine stabilere Software.
Asserts sollten verwendet werden, wenn ein bestimmter Zustand abgeprüft werden soll, der einen Bug aufzeigt. Beispiele sind z.B., wenn der Aufrufer einer Funktion einen NULL-Zeiger übergibt, dieser aber nicht erlaubt ist. Ein weiteres Beispiel ist, wenn eine Klasse eine Initialisierungsreihenfolge erwartet, diese aber vom Benutzer der Klasse nicht korrekt ausgeführt wurde. Ein Assert deckt also Bugs im Programm auf. Gern wird ein Assert benutzt, um Eingangsparameter einer Funktion zu prüfen. Ein möglicher Aufrufer erfährt beim Testen dann schon, ob er die Funktion richtig verwendet (nämlich wenn kein Assert auftritt).
Asserts sind im fertigen Programm nicht mehr vorhanden (da durch Makro zum no-op gemacht oder per Konfiguration ausgeschaltet), können also bei Kundensoftware nicht mehr auftreten. An der Stelle, an der ein Assert auftreten würde, würde das Programm dann einfach von falschen Annahmen ausgehen und abstürzen. Ein klarer Bug, der vom Entwickler gefixt werden müßte. Dies verhindet Bananensoftware (reift beim Kunden) und erzwingt, eine gute Qualitätssicherung und Tests der Software durchzuführen. Auch der Entwickler tut gut daran, mit Unit Tests sicherzustellen, daß seine Klassen auch genau das machen, was sie sollen, und möglichst ohne Asserts (und Bugs).
Vorteil von Asserts sind, daß sie einem sofort zeigen, wo ein Bug ist, und (hat man den Quellcode) sie dokumentieren, welche Erwartungen an den aufrufenden Code gemacht werden. Außerdem verbrauchen sie keinen Speicherplatz und Laufzeit in ausgelieferten Versionen der Software. Nachteil an Asserts ist, daß es schwer ist zu entscheiden, wann ein Assert angebracht ist, oder ob eher eine Exception geworfen werden soll. Das muß man sich am Anfang der Entwicklung überlegen und evtl. in Software-Richtlinien für beteiligte Entwickler festlegen.