Tiefe Einblicke in CLR
Verarbeitung von Ausnahmen bei Beschädigungen
Andrew Pardoe
Dieser Artikel basiert auf einer Vorabversion von Visual Studio 2010. Änderungen an allen Informationen in diesem Artikel sind vorbehalten.
Inhalt
Was genau sind Ausnahmen?
Win32-SEH-Ausnahmen und System.Exception
Verwalteter Code und SEH
Ausnahmen bei Beschädigungen
Die Verwendung von „catch (Exception e)“ ist immer noch falsch
Kluges Codieren
Haben Sie jemals Code geschrieben, der nicht ganz korrekt ist, aber wenigsten gut genug? Haben Sie Code schreiben müssen, der wunderbar funktioniert, solange alles gut geht, wobei Sie aber nicht genau wussten, was geschehen würde, wenn etwas schiefgeht? Es gibt eine einfache, falsche Anweisung, die sich wahrscheinlich in Code befindet, den Sie geschrieben haben oder bearbeiten mussten: catch (Exception e). Sie scheint unschuldig und relativ unkompliziert, aber diese kleine Anweisung kann viele Probleme verursachen, wenn sie nicht das tut, was Sie erwarten.
Wenn Sie jemals Code gesehen haben, der Ausnahmen so wie der folgende Code verwendet, müssen Sie diesen Artikel lesen:
public void FileSave(String name)
{
try
{
FileStream fs = new FileStream(name, FileMode.Create);
}
catch (Exception)
{
throw new System.IO.IOException("File Open Error!");
}
}
Der Fehler in diesem Code kommt häufig vor: Es ist einfacher, Code zum Erfassen aller Ausnahmen zu schreiben, als genau die Ausnahmen zu erfassen, die von dem Code ausgelöst werden könnten, der in Ihrem try-Block ausgeführt wird. Durch Erfassen der Basis der Ausnahmehierarchie wird schließlich aber eine Ausnahme beliebigen Typs „verschluckt“ und in ein IOException konvertiert.
Ausnahmebehandlung ist einer dieser Bereiche, bei denen die meisten Personen über grundlegende Kenntnisse statt über ein tiefes Verständnis verfügen. Ich werde mit ein wenig Hintergrundinformationen beginnen, um die Ausnahmebehandlung vom CLR-Standpunkt aus zu erklären, für diejenigen, die mit Ausnahmebehandlung möglicherweise aus der systemeigenen Programmierung oder einem alten Hochschullehrbuch vertrauter sind. Wenn Sie ein alter Profi sind, was die Behandlung verwalteter Ausnahmen angeht, nehmen Sie sich die Freiheit, zu dem Abschnitt über die verschiedene Arten von Ausnahmen oder zu jenem über verwalteten Code und die Behandlung strukturierter Ausnahmen (structured exception handling, SEH) zu springen. Achten Sie jedoch darauf, dass Sie die letzten Abschnitte lesen.
Was genau sind Ausnahmen?
Eine Ausnahme ist ein Signal, das ausgelöst wird, wenn eine Bedingung erkannt wird, die bei der normalen Ausführung eines Programmthreads nicht erwartet wurde. Viele Agents können falsche Bedingungen erkennen und Ausnahmen auslösen. Programmcode (oder von ihm verwendete Bibliothekcode) kann Typen auslösen, die von System.Exception abgeleitet sind. Das CLR-Ausführungsmodul und nicht verwalteter Code können ebenfalls Ausnahmen auslösen. Ausnahmen, die in einem Ausführungsthread ausgelöst werden, folgen dem Thread durch systemeigenen und verwalteten Code und über AppDomains und werden, wenn sie nicht durch das Programm verarbeitet werden, vom Betriebssystem als unbehandelte Ausnahmen betrachtet.
Eine Ausnahme zeigt an, dass etwas Negatives geschehen ist. Während jede verwaltete Ausnahme einen Typ hat (wie z. B. System.ArgumentException oder System.ArithmeticException), ist der Typ nur in dem Kontext, in dem die Ausnahme ausgelöst wurde, bedeutungsvoll. Ein Programm kann eine Ausnahme verarbeiten, wenn es die Bedingungen versteht, die dazu geführt haben, dass die Ausnahme ausgelöst wurde. Aber wenn das Programm die Ausnahme nicht verarbeitet, könnte dies auf beliebig viele negative Dinge hindeuten. Nachdem die Ausnahme das Programm verlassen hat, hat sie nur eine sehr allgemeine Bedeutung: etwas Negatives ist geschehen.
Wenn Windows sieht, dass ein Programm eine Ausnahme nicht verarbeiten kann, versucht es, die gespeicherten Daten des Programms (Dateien auf einem Datenträger, Registrierungseinstellungen usw.) durch Beenden des Prozesses zu schützen. Selbst wenn die Ausnahme ursprünglich einen ungefährlichen unerwarteten Programmzustand anzeigte (wie z. B. ein Fehlschlagen des Entfernens aus einem leeren Stapel mittels Pop), wird sie scheinbar zu einem ernsten Problem, sobald sie von Windows wahrgenommen wird, da das Betriebssystem keinen Kontext hat, um die Ausnahme richtig zu interpretieren. Ein einzelner Thread in einem AppDomain kann durch das Nichtverarbeiten einer Ausnahme die gesamte CLR-Instanz abstürzen lassen (siehe Abbildung 1).
Abbildung 1 Eine unbehandelte Ausnahme eines Threads führt zur Beendigung des gesamten Prozesses
Wenn Ausnahmen so gefährlich sind, weshalb sind sie dann so beliebt? Es ist wie mit Motorrädern und Kettensägen – die pure Leistungsfähigkeit von Ausnahmen macht sie sehr nützlich. Normaler Datenfluss in einem Programmthread geht über Aufrufe und Rückgaben von Funktion zu Funktion. Jeder Aufruf einer Funktion erstellt einen Ausführungsrahmen im Stapel, jede Rückgabe zerstört diesen Rahmen. Neben der Veränderung des globalen Zustands wird der einzige Datenfluss in einem Programm durch Übergabe von Daten zwischen angrenzenden Rahmen in Form von Funktionsparametern oder Rückgabewerten erreicht. Wenn keine Ausnahmebehandlung vorhanden ist, muss jeder Aufrufer prüfen, ob die von ihm aufgerufene Funktion erfolgreich ausgeführt wird (oder einfach annehmen, dass immer alles OK ist).
Die meisten Win32-APIs geben einen Wert ungleich null zurück, um Fehler anzuzeigen, da Windows keine Ausnahmebehandlung verwendet. Der Programmierer muss in jeden Funktionsaufruf mit Code umschließen, der den Rückgabewert der aufgerufenen Funktion prüft. Dieser Code aus der MSDN-Dokumentation zum Auflisten von Dateien in einem Verzeichnis überprüft z. B. jeden Aufruf explizit auf erfolgreiche Ausführung. Der Aufruf von FindNextFile(...) ist von einer Überprüfung umschlossen, die anzeigt, ob die Rückgabe ungleich null ist. Wenn der Aufruf nicht erfolgreich ist, stellt ein separater Funktionsaufruf – GetLastError() – Details der Ausnahmebedingung zur Verfügung. Beachten Sie, dass jeder Aufruf auf erfolgreiche Ausführung im nächsten Rahmen überprüft werden muss, da Rückgabewerte notwendigerweise auf den lokalen Funktionsbereich begrenzt sind:
// FindNextFile requires checking for success of each call
while (FindNextFile(hFind, &ffd) != 0);
dwError = GetLastError();
if (dwError != ERROR_NO_MORE_FILES)
{
ErrorHandler(TEXT("FindFirstFile"));
}
FindClose(hFind);
return dwError;
Eine Fehlerbedingung kann nur von der Funktion, die die unerwartete Bedingung enthält, zum Aufrufer dieser Funktion übergehen. Ausnahmen haben die Macht, die Ausführungsergebnisse einer Funktion aus dem Bereich der aktuellen Funktion zu jedem Rahmen weiter oben im Stapel zu übergeben, bis der Rahmen erreicht wird, der weiß, wie die unerwartete Bedingung zu verarbeiten ist. Das CLR-Ausnahmesystem (Ausnahmesystem mit zwei Durchläufen genannt) liefert die Ausnahme an jeden Vorgänger in der Aufrufliste des Threads, angefangen beim Aufrufer, und fährt solange fort, bis eine Funktion der Ausnahmebehandlung zustimmt (dies ist der erste Durchlauf).
Das Ausnahmesystem entlädt dann den Zustand jedes Rahmens in der Aufrufliste zwischen dem Ort, wo die Ausnahme ausgelöst wurde, und dem Ort, wo sie verarbeitet wird (dies ist der zweite Durchlauf). Während der Stapelentladung führt die CLR sowohl finally-Klauseln als auch fault-Klauseln in jedem Rahmen aus, sobald er entladen wird. Dann wird die catch-Klausel im behandelnden Rahmen ausgeführt.
Weil die CLR jeden Vorgänger in der Aufrufliste prüft, gibt es keinen Grund für den Aufrufer, einen catch-Block zu haben – die Ausnahme kann irgendwo weiter oben im Stapel verarbeitet werden. Statt den Code sofort das Ergebnis jedes Funktionsaufrufs prüfen zu lassen, kann ein Programmierer Fehler an einem Ort behandeln, der weit von der Stelle entfernt ist, wo die Ausnahme ausgelöst wurde. Beim Verwenden von Fehlercodes muss der Programmierer einen Fehlercode prüfen und bei jeden Stapelrahmen weitergeben, bis er den Ort erreicht, wo die falsche Bedingung behandelt werden kann. Ausnahmebehandlung befreit den Programmierer vom Prüfen der Ausnahme in jedem Rahmen im Stapel.
Weitere Informationen zum Auslösen benutzerdefinierter Ausnahmetypen finden Sie in Fehlerbehandlung: Auslösen benutzerdefinierter Ausnahmetypen von einer verwalteten COM+-Serveranwendung.
Win32-SEH-Ausnahmen und System.Exception
Es gibt eine interessante Nebenwirkung, die durch die Fähigkeit entstanden ist, Ausnahmen weit von dem Ort entfernt zu verarbeiten, wo sie ausgelöst wurden. Ein Programmthread kann Programmausnahmen von jedem aktiven Rahmen in seiner Aufrufliste erhalten, ohne zu wissen, wo die Ausnahme ausgelöst wurde. Ausnahmen repräsentieren aber nicht immer eine Fehlerbedingung, die das Programm erkennt: Ein Programmthread kann auch eine Ausnahme außerhalb des Programms verursachen.
Wenn die Ausführung eines Threads einen Prozessorfehler verursacht, wird die Kontrolle zum Betriebssystemkernel übertragen, der den Fehler dem Thread als SEH-Ausnahme anzeigt. So wie Ihr catch-Block nicht weiß, wo im Stapel des Threads eine Ausnahme ausgelöst wurde, muss er nicht genau wissen, wo der Betriebssystemkernel die SEH-Ausnahme auslöste.
Windows benachrichtigt Programmthreads über Betriebssystemausnahmen mithilfe von SEH. Programmierer von verwaltetem Code werden dies selten sehen, da die CLR in der Regel die von SEH-Ausnahmen angezeigten Fehlerarten verhindert. Wenn Windows aber eine SEH-Ausnahme auslöst, liefert die CLR sie an den verwalteten Code. Obwohl SEH-Ausnahmen in verwaltetem Code selten sind, kann unsicherer verwalteter Code eine STATUS_ACCESS_VIOLATION-Ausnahme generieren, die anzeigt, dass das Programm versucht hat, auf ungültigen Speicher zuzugreifen.
Weitere Einzelheiten zu SEH finden Sie im Artikel von Matt Pietrek Ein Intensivkurs zu den Einzelheiten der Behandlung strukturierter Ausnahmen in Win32 in der Ausgabe des Microsoft Systems Journal vom Januar 1997.
SEH-Ausnahmen sind eine Klasse, die sich von den Ausnahmen unterscheidet, die von Ihrem Programm ausgelöst werden. Ein Programm kann eine Ausnahme auslösen, weil es versucht hat, ein Element mittels Pop aus einem leeren Stapel zu entfernen, oder weil es versuchte, eine nicht vorhandene Datei zu öffnen. Alle diese Ausnahmen sind sinnvoll im Kontext der Ausführung Ihres Programms. SEH-Ausnahmen verweisen auf einen Kontext außerhalb Ihres Programms. Eine Zugriffsverletzung zeigt beispielsweise den Versuch des Schreibens in einen ungültigen Speicher an. Im Unterschied zu Programmfehlern zeigt eine SEH-Ausnahme an, dass die Integrität des Prozesses der Laufzeit beeinträchtigt sein könnte. Selbst wenn SEH-Ausnahmen sich von den Ausnahmen, die von System.Exception abgeleitet werden, unterscheiden, kann die SEH-Ausnahme, wenn die CLR sie an einen verwalteten Thread übergibt, mit der Anweisung „catch (Exception e)“ erfasst werden.
Einige Systeme versuchen, diese zwei Ausnahmearten zu trennen. Wenn Sie Ihr Programm mit dem /EH-Schalter kompilieren, unterscheidet der Microsoft Visual C++-Compiler zwischen Ausnahmen, die von einer C++-throw-Anweisung ausgelöst werden, und Win32-SEH-Ausnahmen. Diese Trennung ist nützlich, weil ein normales Programm nicht weiß, wie Fehler zu behandeln sind, die es nicht ausgelöst hat. Wenn ein C++-Programm versucht, einem std::vector ein Element hinzuzufügen, sollte davon ausgegangen werden, dass der Vorgang fehlschlagen kann, da zu wenig Speicher vorhanden ist, aber bei einem fehlerfreien Programm, das gut geschriebene Bibliotheken verwendet, sollte es keine Probleme mit Zugriffsverletzungen geben.
Diese Trennung ist für Programmierer nützlich. Eine Zugriffsverletzung ist ein ernstes Problem: Ein unerwarteter Schreibvorgang in den kritischen Systemspeicher kann jeden Teil des Prozesses in unvorhersehbarer Weise treffen. Aber einige SEH-Fehler, wie z. B. ein „Division durch null“-Fehler, die aus einer ungültigen (und nicht geprüften) Benutzereingabe resultieren, sind weniger schwerwiegend. Während ein Programm mit einem „Division durch null“-Fehler falsch ist, ist es unwahrscheinlich, dass dies andere Teile des Systems betrifft. Es ist sogar wahrscheinlich, dass ein C++-Programm einen „Division durch null“-Fehler verarbeiten könnte, ohne das übrige System zu destabilisieren. Obwohl diese Trennung also nützlich ist, drückt sie nicht die verwaltete Semantik aus, die Programmierer benötigen.
Verwalteter Code und SEH
Die CLR hat SEH-Ausnahmen immer mit den gleichen Methoden an verwalteten Code geliefert wie die Ausnahmen, die vom Programm selbst ausgelöst wurden. Dies ist kein Problem, solange der Code nicht versucht, Ausnahmebedingungen zu verarbeiten, wenn er nicht wirklich dazu in der Lage ist. Die meisten Programme können nach einer Zugriffsverletzung die Ausführung nicht sicher fortsetzen. Leider hat das Ausnahmebehandlungsmodell der CLR Benutzer immer dazu ermutigt, diese schwerwiegenden Fehler zu erfassen, indem Programmen ermöglicht wurde, alle Ausnahmen ganz oben in der System.Exception-Hierarchie zu erfassen. Aber dieses Verfahren ist selten richtig.
Das Schreiben von „catch (Exception e)“ ist ein häufiger Programmierfehler, da unbehandelte Ausnahmen ernste Folgen haben. Sie könnten aber argumentieren, dass, wenn Sie nicht wissen, welche Fehler von einer Funktion ausgelöst werden, Sie einen Schutz vor allen möglichen Fehlern gewährleisten sollten, wenn Ihr Programm diese Funktion aufruft. Dies scheint ein vernünftiger Ansatz zu sein, bis Sie daran denken, was es bedeutet, die Ausführung fortzusetzen, wenn Ihr Prozess möglicherweise beschädigt ist. Manchmal ist das Abbrechen und ein erneuter Versuch die beste Option: Niemand möchte einen Watson-Dialog sehen, aber es ist besser, das Programm neu zu starten, als Ihre Daten zu beschädigen.
Wenn Programme Ausnahmen erfassen, die aus Kontexten entstehen, die diese Programme nicht verstehen, ist das ein ernstes Problem. Das Problem kann jedoch nicht mithilfe von Ausnahmespezifikationen oder einer anderen Vertragsmethode gelöst werden. Es ist außerdem wichtig, dass verwaltete Programme Benachrichtigungen über SEH-Ausnahmen erhalten können, da die CLR eine Plattform für viele Arten von Anwendungen und Hosts ist. Einige Hosts, wie z. B. SQL Server, müssen die vollständige Kontrolle über den Prozess ihrer Anwendung haben. Verwalteter Code, der mit dem systemeigenen Code zusammenarbeitet, muss manchmal systemeigene C++-Ausnahmen oder SEH-Ausnahmen behandeln.
Die meisten Programmierer, die „catch (Exception e)“ schreiben, wollen aber nicht wirklich Zugriffsverletzungen erfassen. Sie bevorzugen es, dass die Ausführung ihrer Programme angehalten wird, wenn ein schwerwiegender Fehler auftritt, statt das Programm weiter in einem unbekannten Zustand weitermachen zu lassen. Dies gilt insbesondere für Programme, die verwaltete Add-Ins wie z. B. Visual Studio oder Microsoft Office hosten. Wenn ein Add-In eine Zugriffsverletzung verursacht und dann die Ausnahme „verschluckt“, könnte der Host einen Schaden an seinem eigenen Zustand (oder den Benutzerdateien) verursachen, ohne jemals zu erkennen, dass etwas schiefgegangen ist.
In Version 4 der CLR entwickelt das Produktteam Ausnahmen, die einen beschädigten Prozesszustand anzeigen und sich von allen anderen Ausnahmen unterscheiden. Wir stellen über ein Dutzend SEH-Ausnahmen bereit, um einen beschädigten Prozesszustand anzuzeigen. Diese Ausnahmen sind jeweils auf den Kontext bezogen, in dem sie ausgelöst werden, statt auf den Ausnahmetyp selbst. Dies bedeutet, dass eine Zugriffsverletzung, die von Windows empfangen wird, als Ausnahme infolge einer Beschädigung (corrupted state exception, CSE) markiert wird, während eine, die im Benutzercode durch Schreiben von „throw new System.AccessViolationException“ ausgelöst wird, nicht als CSE markiert wird. Wenn Sie an der PDC 2008 teilgenommen haben, haben Sie eine Community Technology Preview von Visual Studio 2010 erhalten, die diese Änderungen enthält.
Es ist wichtig zu beachten, dass die Ausnahme den Prozess nicht beschädigt: Die Ausnahme wird ausgelöst, nachdem eine Beschädigung im Prozesszustand erkannt wurde. Wenn z. B. ein Schreibvorgang über einen Zeiger in unsicherem Code auf Speicher verweist, der nicht zum Programm gehört, wird eine Zugriffsverletzung ausgelöst. Der illegale Schreibvorgang ist in Wirklichkeit nicht erfolgt – das Betriebssystem hat die Speicherzugehörigkeit überprüft und die Aktionsausführung verhindert. Die Zugriffsverletzung zeigt an, dass der Zeiger selbst zu einem früheren Zeitpunkt bei der Ausführung des Threads beschädigt wurde.
Ausnahmen bei Beschädigungen
In Version 4 und höher wird das CLR-Ausnahmesystem keine CSEs für verwalteten Code bereitstellen, es sei denn, der Code hat ausdrücklich angezeigt, dass er Ausnahmen infolge beschädigter Prozesse verarbeiten kann. Dies bedeutet, dass einer Instanz von „catch (Exception e)“ in verwaltetem Code keine CSE präsentiert wird. Aufgrund der Änderung innerhalb des CLR-Ausnahmesystems müssen Sie nicht die Ausnahmehierarchie ändern oder irgendeine Ausnahmebehandlungssemantik irgendeiner verwalteten Sprache anpassen.
Aus Kompatibilitätsgründen hat das CLR-Team einige Möglichkeiten zur Verfügung gestellt, damit Sie Ihren alten Code mit dem alten Verhalten ausführen können:
- Wenn Sie Ihren Code, der in Microsoft .NET Framework 3.5 erstellt wurde, erneut kompilieren und in .NET Framework 4.0 ausführen möchten, ohne die Quelle aktualisieren zu müssen, können Sie einen Eintrag in Ihrer Anwendungskonfigurationsdatei hinzufügen: legacyCorruptedStateExceptionsPolicy=true.
- Assemblys, die mit .NET Framework 3.5 oder einer früheren Version der Laufzeit kompiliert wurden, werden Ausnahmen infolge einer Beschädigung verarbeiten können (also das alte Verhalten beibehalten), wenn sie unter .NET Framework 4.0 ausgeführt werden.
Wenn Sie möchten, dass Ihr Code CSEs verarbeitet, müssen Sie Ihre Absicht anzeigen, indem Sie die Funktion, die die Ausnahmenklausel enthält (catch, finally oder fault), mit einem neuen Attribut markieren: System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptions. Wenn eine CSE ausgelöst wird, führt die CLR ihre Suche nach einer entsprechenden catch-Klausel durch, wird aber nur in Funktionen suchen, die mit dem HandleProcessCorruptedStateExceptions-Attribut markiert sind (siehe Abbildung 2).
Abbildung 2 Verwenden von HandleProcessCorruptedStateExceptions
// This program runs as part of an automated test system so you need
// to prevent the normal Unhandled Exception behavior (Watson dialog).
// Instead, print out any exceptions and exit with an error code.
[HandledProcessCorruptedStateExceptions]
public static int Main()
{
try
{
// Catch any exceptions leaking out of the program
CallMainProgramLoop();
}
catch (Exception e) // We could be catching anything here
{
// The exception we caught could have been a program error
// or something much more serious. Regardless, we know that
// something is not right. We'll just output the exception
// and exit with an error. We won't try to do any work when
// the program or process is in an unknown state!
System.Console.WriteLine(e.Message);
return 1;
}
return 0;
}
Wenn eine passende catch-Klausel gefunden wird, wird die CLR den Stapel normal entladen, aber nur die finally- und fault-Blöcke (und in C# den impliziten finally-Block einer using-Anweisung) in Funktionen ausführen, die mit dem Attribut markiert sind. Das HandleProcessCorruptedStateExceptions-Attribut wird ignoriert, wenn es in teilweise vertrauenswürdigem oder transparentem Code angetroffen wird, weil ein vertrauenswürdiger Host nicht wollen würde, dass ein nicht vertrauenswürdiges Add-In diese schwerwiegenden Ausnahmen erfasst und ignoriert.
Die Verwendung von „catch (Exception e)“ ist immer noch falsch
Obwohl das CLR-Ausnahmesystem die schlimmsten Ausnahmen als CSE markiert, ist es immer noch keine gute Idee, „catch (Exception e)“ in Ihren Code zu schreiben. Ausnahmen repräsentieren eine ganze Palette an unerwarteten Situationen. Die CLR kann die schlimmsten Ausnahmen – SEH-Ausnahmen – erkennen, die einen möglicherweise beschädigten Prozesszustand anzeigen. Andere unerwarteten Bedingungen können aber immer noch schädlich sein, wenn sie ignoriert oder generisch behandelt werden.
Wenn keine Prozessbeschädigung vorhanden ist, bietet die CLR ziemlich starke Garantien zu Programmkorrektheit und Speichersicherheit. Beim Ausführen eines Programms, das in sicherem MSIL-Code (Microsoft Intermediate Language) geschrieben ist, können Sie sicher sein, dass alle Anweisungen in Ihrem Programm richtig ausgeführt werden. Zu tun, was die Programmanweisungen vorgeben, unterscheidet sich aber oft davon, was der Programmierer möchte. Ein Programm, das der CLR zufolge vollkommen richtig ist, kann einen gespeicherten Zustand beschädigen, wie z. B. auf einen Datenträger geschriebene Programmdateien.
Sehen wir uns ein einfaches Beispiel ein Programm an, das eine Datenbank mit Prüfungsergebnissen für eine Schule verwaltet. Das Programm verwendet objektorientierte Entwurfsprinzipien, um die Daten einzukapseln, und löst verwaltete Ausnahmen aus, um unerwartete Ereignisse anzuzeigen. Eines Tages drückt die Schulsekretärin beim Generieren einer Notendatei die Eingabetaste einmal zu oft. Das Programm versucht, einen Wert mittels Pop aus einer leeren Warteschlange zu entfernen, und löst ein QueueEmptyException aus, das unbehandelt Rahmen in der Aufrufliste durchläuft.
Irgendwo nahe der Spitze des Stapels gibt es eine Funktion namens „GenerateGrades()“ mit einer try/catch-Klausel, die die Ausnahme erfasst. Leider hat GenerateGrades() keine Ahnung davon, dass Studenten in einer Warteschlange gespeichert sind, und weiß auch nicht, was mit einem QueueEmptyException zu tun ist. Aber der Programmierer, der GenerateGrades() geschrieben hat, will nicht, dass das Programm abstürzt, ohne die bisher berechneten Daten zu speichern. Alles wird sicher auf den Datenträger geschrieben und das Programm wird beendet.
Das Problem bei diesem Programm ist, dass es einige Annahmen macht, die falsch sein könnten. Warum sollte sich der fehlende Eintrag in der Schülerwarteschlange am Ende befinden? Vielleicht wurde der erste Schülerdatensatz ausgelassen, oder vielleicht der zehnte. Die Ausnahme teilt dem Programmierer nur mit, dass das Programm falsch ist. Das Ausführen irgendeiner Aktion – Speichern der Daten auf einem Datenträger oder „Wiederherstellen“ und Fortsetzen der Ausführung – ist einfach falsch. Keine richtige Aktion ist ohne Kenntnis des Kontexts möglich, in dem die Ausnahme ausgelöst wurde.
Wenn das Programm eine spezifische Ausnahme nahe dem Ort, wo die Ausnahme ausgelöst wurde, erfasst hätte, hätte es angemessene Maßnahmen durchführen können. Das Programm weiß, was QueueEmptyException in der Funktion bedeutet, die versucht, Schüler aus der Warteschlange zu entfernen. Wenn die Funktion diese Ausnahme nach Typ erfasst, statt eine ganze Klasse von Ausnahmetypen zu erfassen, wäre sie viel besser in der Lage, den Programmzustand zu korrigieren.
Im Allgemeinen ist das Erfassen einer spezifischen Ausnahme die richtige Vorgehensweise, da so der meiste Kontext für Ihren Ausnahmehandler bereitgestellt wird. Wenn Ihr Code potenziell zwei Ausnahmen erfassen kann, muss er beide verarbeiten können. Beim Schreiben von Code, der sagt „catch (Exception e)“, muss wirklich jede Ausnahmesituation verarbeitet werden können. Dies ist ein Versprechen, das sehr schwer eingehalten werden kann.
Einige Sprachen versuchen, Programmierer daran zu hindern, eine umfassende Klasse an Ausnahmen zu erfassen. C++ hat zum Beispiel Ausnahmespezifikationen – einen Mechanismus, der einem Programmierer ermöglicht festzulegen, welche Ausnahmen in dieser Funktion ausgelöst werden können. Java führt diesen Mechanismus einen Schritt weiter und setzt dabei geprüfte Ausnahmen ein – eine vom Compiler durchgesetzte Anforderung, dass eine bestimmte Klasse von Ausnahmen angegeben werden muss. In beiden Sprachen listen Sie in der Funktionsdeklaration die Ausnahmen auf, die aus dieser Funktion herauskommen können, und Aufrufer werden angehalten, diese Ausnahmen zu behandeln. Ausnahmespezifikationen sind eine gute Idee, aber in der Praxis haben sie gemischte Ergebnisse hervorgebracht.
Es wird heftig darüber diskutiert, ob verwalteter Code CSEs behandeln können sollte oder nicht. Diese Ausnahmen kennzeichnen normalerweise einen Fehler auf Systemebene und sollten nur von Code behandelt werden, der den Systemebenenkontext versteht. Während die meisten Menschen keine CSEs behandeln können müssen, gibt es einige Szenarios, in denen es notwendig ist.
Ein solches Szenario ist, wenn Sie sehr nahe an der Stelle sind, wo die Ausnahme aufgetreten ist. Denken Sie zum Beispiel an ein Programm, das systemeigenen Code aufruft, der als fehlerhaft bekannt ist. Sie wissen, dass das Debuggen im Code manchmal den Zeiger auf null setzt, bevor auf ihn zugegriffen werden kann, was eine Zugriffsverletzung verursacht. Sie möchten vielleicht das HandleProcessCorruptedStateExceptions-Attribut für die Funktion verwenden, die mithilfe von P/Invoke den systemeigenen Code aufruft, weil Sie die Ursache für die Zeigerbeschädigung kennen und zufrieden sind, dass die Prozessintegrität gewahrt wird.
Das andere Szenario, das möglicherweise nach der Verwendung dieses Attributs verlangt, liegt vor, wenn Sie so weit vom Fehler entfernt sind, wie Sie nur sein können. Sie stehen kurz davor, Ihren Prozess zu beenden. Nehmen wir an, Sie haben einen Host oder ein Framework geschrieben, das beim Auftreten eines Fehlers eine benutzerdefinierte Protokollierung durchführen will. Sie könnten Ihre Hauptfunktion mit einem try/catch/finally-Block umschließen und mit HandleProcessCorruptedStateExceptions markieren. Wenn ein Fehler unerwartet bis zur Hauptfunktion Ihres Programms vordringt, schreiben Sie einige Daten in Ihr Protokoll, wobei Sie sich so wenig Arbeit wie möglich machen, und beenden den Prozess. Wenn die Integrität des Prozesses gefährdet wird, könnte jeder Schritt gefährlich sein, aber wenn die benutzerdefinierte Protokollierung manchmal fehlschlägt, ist das akzeptabel.
Betrachten Sie das Diagramm in Abbildung 3. Hier ist die Funktion 1 (fn1()) mit dem Attribut [HandleProcessCorruptedStateExceptions] ausgestattet, sodass ihre catch-Klausel die Zugriffsverletzung erfasst. Der finally-Block in der Funktion 3 wird nicht ausführt, obwohl die Ausnahme in Funktion 1 erfasst wird. Funktion 4 unten im Stapel löst eine Zugriffsverletzung aus.
Abbildung 3 Ausnahme und Zugriffsverletzung
Es gibt in keinem dieser Szenarios eine Garantie, dass das, was Sie tun, vollkommen sicher ist, aber es gibt Szenarios, bei denen nur das Beenden des Prozesses unakzeptabel ist. Wenn Sie sich jedoch dafür entscheiden, eine CSE zu behandeln, lastet auf Ihnen als Programmierer eine riesige Bürde, es richtig zu machen. Erinnern Sie sich daran, dass das CLR-Ausnahmesystem für eine Funktion nicht einmal eine CSE bereitstellt, wenn die Funktion nicht entweder während des ersten Durchlaufs (wenn nach einer geeigneten catch-Klausel gesucht wird) oder beim zweiten Durchlauf (wenn der Zustand jedes Rahmens entladen wird und finally- und fault-Blöcke ausgeführt werden) mit dem neuen Attribut markiert wurde.
Der finally-Block existiert, um zu garantieren, dass Code immer ausgeführt wird, unabhängig davon, ob es eine Ausnahme gibt oder nicht. (Fault-Blöcke werden nur beim Auftreten einer Ausnahme ausgeführt, aber sie haben eine ähnliche Garantie, immer ausgeführt zu werden.) Diese Konstrukte werden zum Bereinigen kritischer Ressourcen verwendet, wie z. B. beim Freigeben von Dateihandles oder beim Umkehren von Identitätswechselkontexten.
Sogar Code, der durch Verwendung eingeschränkter Ausführungsbereiche (constrained execution region, CER) zuverlässig gemacht werden soll, wird beim Auslösen einer CSE nicht ausgeführt, es sei denn, er befindet sich in einer Funktion, die mit dem HandleProcessCorruptedStateExceptions-Attribut markiert wurde. Es ist sehr schwierig, den korrekten Code zu schreiben, der eine CSE verarbeitet und den Prozess weiterhin sicher ausführt.
Schauen wir uns den Code in Abbildung 4 genauer an, um zu sehen, was alles schiefgehen könnte. Wenn sich dieser Code nicht in einer Funktion befindet, die CSEs verarbeiten kann, wird der finally-Block bei einer Zugriffsverletzung nicht ausgeführt. Dies ist in Ordnung, wenn der Prozess beendet wird – das offene Dateihandle wird freigegeben. Aber wenn anderer Code die Zugriffsverletzung erfasst und versucht, den Zustand wiederherzustellen, muss ihm bekannt sein, dass er diese Datei schließen und alle anderen externen Zustände wiederherstellen muss, die dieses Programm geändert hat.
Abbildung 4 finally-Block kann möglicherweise nicht ausgeführt werden
void ReadFile(int index)
{
System.IO.StreamReader file =
new System.IO.StreamReader(filepath);
try
{
file.ReadBlock(buffer, index, buffer.Length);
}
catch (System.IO.IOException e)
{
Console.WriteLine("File Read Error!");
}
finally
{
if (file != null)
{
file.Close()
}
}
}
Wenn Sie sich dafür entscheiden, eine CSE zu behandeln, muss Ihr Code damit rechnen, dass es einen äußerst umfangreichen kritischen Zustand gibt, der noch nicht entladen wurde. finally- und fault-Blöcke wurden nicht ausgeführt. Eingeschränkte Ausführungsbereiche wurden nicht ausgeführt. Das Programm und der Prozess befinden sich in einem unbekannten Zustand.
Wenn Sie wissen, dass Ihr Code sich richtig verhalten wird, wissen Sie, was zu tun ist. Wenn Sie sich aber nicht sicher sind, in welchem Zustand Ihr Programm ausgeführt wird, ist es besser, Ihren Prozess beenden zu lassen. Wenn Ihre Anwendung gehostet wird, rufen Sie die von Ihrem Host angegebene Eskalationsrichtlinie auf. Weitere Informationen zum Schreiben von zuverlässigem Code und CERs finden Sie im Artikel von Alessandro Catorcini und Brian Grunkemeyer in der Rubrik Tiefe Einblicke in CLR in der Ausgabe von Dezember 2007.
Kluges Codieren
Obwohl die CLR Sie daran hindert, naiv CSEs zu erfassen, ist es immer noch keine gute Idee, übermäßig umfangreiche Klassen von Ausnahmen zu erfassen. „catch (Exception e)“ erscheint aber sehr oft in Code, und es ist unwahrscheinlich, dass dies sich ändern wird. Indem Sie die Ausnahmen, die einen beschädigten Prozesszustand repräsentieren, nicht an Code liefern, der alle Ausnahmen naiv erfasst, halten Sie diesen Code davon ab, eine ernste Situation weiter zu verschlimmern.
Wenn Sie das nächste Mal Code schreiben oder pflegen, der eine Ausnahme erfasst, denken Sie darüber nach, was die Ausnahme bedeutet. Entspricht der von Ihnen erfasste Typ dem, was Ihr Programm (und Bibliotheken, die es verwendet) der Dokumentation gemäß auslöst? Wissen Sie, wie Sie die Ausnahme behandeln müssen, damit Ihr Programm weiter richtig und sicher ausgeführt werden kann?
Ausnahmebehandlung ist ein leistungsfähiges Tool, das vorsichtig und mit Bedacht verwendet werden sollte. Wenn Sie dieses Feature wirklich verwenden müssen – wenn Sie wirklich Ausnahmen behandeln müssen, die möglicherweise einen beschädigten Prozess anzeigen –, wird die CLR Ihnen vertrauen und Sie dies tun lassen. Seien Sie nur vorsichtig, und machen Sie es richtig.
Senden Sie Fragen und Kommentare in englischer Sprache an clrinout@microsoft.com.
Andrew Pardoe ist Programmmanager für CLR bei Microsoft. Er bearbeitet viele Aspekte des Ausführungsmoduls sowohl für die Desktop- als auch die Silverlight-Laufzeit. Andrew Pardoe kann unter Andrew.Pardoe@microsoft.com erreicht werden.