Ausnahmen sind unvermeidliche Probleme bei der Verwendung von .NET, aber zu viele Entwickler berücksichtigen dieses Problem nicht aus der Perspektive des API-Designs. Bei den meisten Jobs wissen sie von Anfang bis Ende, welche Ausnahmen abgefangen und welche Ausnahmen in das globale Protokoll geschrieben werden müssen. Wenn Sie eine API entwerfen, die Ihnen die korrekte Verwendung von Ausnahmen ermöglicht, können Sie die Zeit, die zur Behebung von Fehlern benötigt wird, erheblich verkürzen.
Wessen Schuld?
Die grundlegende Theorie hinter dem Ausnahmedesign beginnt mit der Frage: „Wessen Schuld?“ Für die Zwecke dieses Artikels wird die Antwort auf diese Frage immer eine der folgenden drei sein:
Bibliothek
Anwendung
Umgebung
Wenn wir sagen, dass es ein Problem mit einer „Bibliothek“ gibt, meinen wir, dass eine der aktuell ausgeführten Methoden einen internen Fehler aufweist. In diesem Fall ist die „Anwendung“ der Code, der die Bibliotheksmethoden aufruft (das ist etwas verwirrend, da sich die Bibliothek und der Anwendungscode möglicherweise in derselben Assembly befinden.) Schließlich bezieht sich die „Umgebung“ auf alles außerhalb der Anwendung. Etwas, das nicht kontrolliert werden kann.
Bibliotheksfehler
Der typischste Bibliotheksfehler ist NullReferenceException. Es gibt keinen Grund für eine Bibliothek, eine Nullreferenzausnahme auszulösen, die von der Anwendung erkannt werden kann. Wenn ein Nullwert auftritt, sollte der Bibliothekscode immer eine spezifischere Ausnahme auslösen, die beschreibt, was der Nullwert ist und wie das Problem behoben werden kann. Bei Parametern handelt es sich offensichtlich um eine ArgumentNullException. Und wenn die Eigenschaft oder das Feld leer ist, ist InvalidOperationException normalerweise besser geeignet.
Per Definition ist jede Ausnahme, die auf einen Bibliotheksfehler hinweist, ein Fehler in dieser Bibliothek, der behoben werden muss. Das heißt nicht, dass der Anwendungscode fehlerfrei ist, sondern dass die Fehler der Bibliothek zuerst behoben werden müssen. Nur dann kann der Anwendungsentwickler wissen, dass auch er einen Fehler gemacht hat.
Der Grund dafür ist, dass möglicherweise viele Personen dieselbe Bibliothek nutzen. Wenn eine Person fälschlicherweise Null an einer Stelle übergibt, an der sie es nicht tun sollte, werden andere zwangsläufig denselben Fehler machen. Durch Ersetzen von NullReferenceException durch eine Ausnahme, die deutlich zeigt, was schief gelaufen ist, wissen Anwendungsentwickler sofort, was schief gelaufen ist.
„Die Grube des Erfolgs“
Wenn Sie die frühe Literatur zu .NET-Entwurfsmustern lesen, werden Sie oft auf den Ausdruck „Die Grube des Erfolgs“ stoßen. Die Grundidee besteht darin, den Code so zu gestalten, dass er leicht richtig verwendet werden kann, ihn aber schwer zu verwenden, wenn er falsch verwendet wird, und sicherzustellen, dass Ausnahmen Ihnen sagen können, was schief gelaufen ist. Durch die Befolgung dieser API-Designphilosophie ist es für Entwickler fast garantiert, dass sie von Anfang an korrekten Code schreiben.
Aus diesem Grund ist eine unkommentierte NullReferenceException so schlimm. Abgesehen von einem Stack-Trace (der sehr tief im Bibliothekscode liegen kann) gibt es keine Informationen, die dem Entwickler helfen könnten, herauszufinden, was er falsch macht. ArgumentNullException und InvalidOperationException hingegen bieten Bibliotheksautoren die Möglichkeit, Anwendungsentwicklern zu erklären, wie das Problem behoben werden kann.
Andere Bibliotheksfehler
Der nächste Bibliotheksfehler ist die ArithmeticException-Reihe, einschließlich DivideByZeroException, FiniteNumberException und OverflowException. Auch dies bedeutet immer einen internen Fehler in der Bibliotheksmethode, auch wenn es sich bei diesem Fehler lediglich um eine fehlende Parametergültigkeitsprüfung handelt.
Ein weiteres Beispiel für einen Bibliotheksfehler ist IndexOutOfRangeException. Semantisch unterscheidet es sich nicht von ArgumentOutOfRangeException, siehe IList.Item, funktioniert aber nur mit Array-Indexern. Da Anwendungscode normalerweise keine nackten Arrays verwendet, bedeutet dies, dass benutzerdefinierte Sammlungsklassen Fehler aufweisen.
ArrayTypeMismatchException ist seit der Einführung generischer Listen in .NET 2.0 selten geworden. Die Umstände, die diese Ausnahme auslösen, sind ziemlich seltsam. Laut Dokumentation:
ArrayTypeMismatchException wird ausgelöst, wenn das System ein Array-Element nicht in den deklarierten Array-Typ konvertieren kann. Beispielsweise kann ein Element vom Typ String nicht in einem Array von Int32 gespeichert werden, da keine Konvertierung zwischen den beiden Typen erfolgt. Anwendungen müssen solche Ausnahmen im Allgemeinen nicht auslösen.
Dazu muss das zuvor erwähnte Int32-Array in einer Variablen vom Typ Object[] gespeichert werden. Wenn Sie ein Roharray verwendet haben, muss die Bibliothek dies überprüfen. Aus diesem und vielen anderen Gründen ist es besser, keine Roharrays zu verwenden, sondern sie in einer geeigneten Sammlungsklasse zu kapseln.
Üblicherweise spiegeln sich andere Casting-Probleme in InvalidCastException-Ausnahmen wider. Zurück zu unserem Thema: Die Typprüfung sollte bedeuten, dass niemals eine InvalidCastException, sondern eine ArgumentException oder InvalidOperationException an den Aufrufer geworfen wird.
MemberAccessException ist eine Basisklasse, die verschiedene reflexionsbasierte Fehler abdeckt. Zusätzlich zur direkten Verwendung von Reflection können COM-Interop und die falsche Verwendung dynamischer Schlüsselwörter diese Ausnahme auslösen.
App-Defekte
Typische Anwendungsfehler sind ArgumentException und seine Unterklassen ArgumentNullException und ArgumentOutOfRangeException. Hier sind weitere Unterklassen, die Sie möglicherweise nicht kennen:
System.ComponentModel.InvalidAsynchronousStateException
System.ComponentModel.InvalidEnumArgumentException
System.DuplicateWaitObjectException
System.Globalization.CultureNotFoundException
System.IO.Log.ReservationNotFoundException
System.Text.DecoderFallbackException
System.Text.EncoderFallbackException
All dies zeigt deutlich, dass die Anwendung vorhanden ist ist ein Fehler und das Problem liegt in der Zeile, die die Bibliotheksmethode aufruft. Beide Teile dieser Aussage sind wichtig. Betrachten Sie den folgenden Code:
foo.Customer = null; foo.Save();
Wenn der obige Code eine ArgumentNullException auslöst, ist der Anwendungsentwickler verwirrt. Es sollte eine InvalidOperationException auslösen, die darauf hinweist, dass vor der aktuellen Zeile ein Fehler aufgetreten ist.
Ausnahmen dokumentieren
Der typische Programmierer liest keine Dokumentation, zumindest nicht überhaupt. Stattdessen liest er oder sie die öffentliche API, schreibt Code und führt ihn aus. Wenn der Code nicht ordnungsgemäß ausgeführt wird, suchen Sie im Stack Overflow nach Ausnahmeinformationen. Wenn der Programmierer Glück hat, ist es dort leicht, die Antwort zusammen mit einem Link zur richtigen Dokumentation zu finden. Aber selbst dann werden Programmierer es wahrscheinlich nicht wirklich lesen.
Wie lösen wir als Bibliotheksautoren dieses Problem? Der erste Schritt besteht darin, das Teildokument direkt in die Ausnahme zu kopieren.
Weitere Objektstatusausnahmen
InvalidOperationException verfügt über eine bekannte Unterklasse ObjectDisposedException. Der Zweck liegt auf der Hand, allerdings vergessen nur wenige zerstörbare Klassen, diese Ausnahme auszulösen. Wenn man es vergisst, ist ein häufiges Ergebnis eine NullReferenceException. Diese Ausnahme wird dadurch verursacht, dass die Dispose-Methode das zerstörbare untergeordnete Objekt auf null setzt.
In engem Zusammenhang mit InvalidOperationException steht die NotSupportedException-Ausnahme. Die beiden Ausnahmen sind leicht zu unterscheiden: InvalidOperationException bedeutet „Sie können das jetzt nicht tun“, während NotSupportedException bedeutet „Sie können diese Operation nie für diese Klasse ausführen“. Theoretisch sollte NotSupportedException nur bei Verwendung abstrakter Schnittstellen auftreten.
Zum Beispiel sollte eine unveränderliche Sammlung eine NotSupportedException auslösen, wenn sie auf die IList.Add-Methode trifft. Im Gegensatz dazu löst eine einfrierbare Sammlung eine InvalidOperationException aus, wenn diese Methode im eingefrorenen Zustand auftritt.
Eine immer wichtiger werdende Unterklasse von NotSupportedException ist PlatformNotSupportedException. Diese Ausnahme weist darauf hin, dass der Vorgang in einigen Betriebsumgebungen ausgeführt werden kann, in anderen jedoch nicht. Beispielsweise müssen Sie diese Ausnahme möglicherweise verwenden, wenn Sie Code von .NET nach UWP oder .NET Core portieren, da diese nicht alle Funktionen des .NET Framework bereitstellen.
The Elusive FormatException
Microsoft hat beim Entwurf der ersten Version von .NET einige Fehler gemacht. Beispielsweise ist FormatException logischerweise ein Parameterausnahmetyp, auch wenn in der Dokumentation steht: „Diese Ausnahme wird ausgelöst, wenn das Parameterformat ungültig ist.“ Allerdings erbt es ArgumentException aus irgendeinem Grund nicht wirklich. Es gibt auch keinen Platz zum Speichern von Parameternamen.
Unser vorübergehender Vorschlag besteht nicht darin, FormatException auszulösen, sondern selbst eine Unterklasse von ArgumentException zu erstellen, die „ArgumentFormatException“ oder andere Namen mit ähnlichen Auswirkungen heißen kann. Dadurch können Sie die erforderlichen Informationen wie Parameternamen und tatsächlich verwendete Werte erhalten und so die Debugging-Zeit verkürzen.
Damit sind wir wieder beim ursprünglichen Thema „Außergewöhnliches Design“. Ja, Sie können einfach eine FormatException auslösen, wenn Ihr selbst entwickelter Parser ein Problem erkennt, aber das hilft Anwendungsentwicklern, die Ihre Bibliothek verwenden möchten, nicht.
Ein weiteres Beispiel für diesen Framework-Designfehler ist IndexOutOfRangeException. Semantisch unterscheidet es sich nicht von ArgumentOutOfRangeException. Gilt dieser Sonderfall jedoch nur für Array-Indexer? Nein, es wäre falsch, so zu denken. Betrachtet man den Instanzsatz von IList.Item, löst diese Methode nur ArgumentOutOfRangeException aus.
Umweltmängel
Umweltmängel entstehen durch die Tatsache, dass die Welt nicht perfekt ist, wie z. B. Datenausfallzeiten, mangelnde Reaktionsfähigkeit des Webservers, Dateiverlust usw. Wenn in einem Fehlerbericht ein Umgebungsfehler auftritt, müssen zwei Aspekte berücksichtigt werden:
Wurde der Fehler von der Anwendung korrekt behandelt?
Was verursacht Fehler in dieser Umgebung?
Typischerweise handelt es sich dabei um eine Arbeitsteilung. Erstens sollten Anwendungsentwickler die Ersten sein, die nach Antworten auf ihre Fragen suchen. Dabei geht es nicht nur um die Fehlerbehandlung und -behebung, sondern auch um die Erstellung eines nützlichen Protokolls.
你可能想知道,为什么要从应用程序开发人员开始。应用程序开发人员要对运维团队负责。如果一次Web服务器调用失败,则应用程序开发人员不能只是甩手大叫“不是我的问题”。他或她首先需要确保异常提供了足够的细节信息,让运维人员可以开展他们的工作。如果异常仅仅提供了“服务器连接超时”的信息,那么他们怎么能知道涉及了哪台服务器?
专用异常
NotImplementedException
NotImplementedException表示且仅表示一件事:这项特性还在开发过程中。因此,NotImplementedException提供的信息应该总是包含一个任务跟踪软件的引用。例如:
throw new NotImplementedException("参见工单#42.");
你可以提供更详细的信息,但实际上,你记录的任何信息几乎立刻就会过期。因此,最好是只将读者导向工单,他们可以在那里看到诸如该特性按计划将会在何时实现这样的信息。
AggregateException
AggregateException是必要之恶,但很难使用。它本身不包含任何有价值的信息,所有的细节信息都隐藏在它的InnerExceptions集合中。
由于AggregateException通常只包含一个项,所以在库中将它解封装并返回真正的异常似乎是合乎逻辑的。一般来说,你不能在没有销毁原始堆栈跟踪的情况下再次抛出一个内部异常,但从.NET 4.5开始,该框架提供了使用ExceptionDispatchInfo的方法。
解封装AggregateException
catch (AggregateException ex) { if (ex.InnerExceptions.Count == 1) //解封装 ExceptionDispatchInfo.Capture(ex.InnerExceptions[0]).Throw(); else throw; //我们真的需要AggregateException }
无法回答的情况
有一些异常无法简单地纳入这个主题。例如,AccessViolationException表示读取非托管内存时有问题。对,那可能是由原生库代码所导致的,也可能是由应用程序错误地使用了同样的代码库所导致的。只有通过研究才能揭示这个Bug的本质。
如果可能,你就应该在设计时避免无法回答的异常。在某些情况下,Visual Studio的静态代码分析器甚至可以分析该规则所涵盖的标识冲突。
例如,ApplicationException实际上已经废弃。Framework设计指南明确指出,“不要抛出或继承ApplicationException。”为此,应用程序不必抛出ApplicationException异常。虽说初衷如此,但看下下面这些子类:
Microsoft.JScript.BreakOutOfFinally
Microsoft.JScript.ContinueOutOfFinally
Microsoft.JScript.JScriptException
Microsoft.JScript.NoContextException
Microsoft.JScript.ReturnOutOfFinally
System.Reflection.InvalidFilterCriteriaException
System.Reflection.TargetException
System.Reflection.TargetInvocationException
System.Reflection.TargetParameterCountException
System.Threading.WaitHandleCannotBeOpenedException
显然,这些子类中有一些应该是参数异常,而其他的则表示环境问题。它们全都不是“应用程序异常”,因为他们只会被.NET Framework的库抛出。
同样的道理,开发人员不应该直接使用SystemException。同ApplicationException一样,SystemException的子类也是各不相同,包括ArgumentException、NullReferenceException和AccessViolationException。微软甚至建议忘掉SystemException的存在,而只使用其子类。
无法回答的情况有一个子类别,就是基础设施异常。我们已经看过AccessViolationException,以下是其他的基础设施异常:
CannotUnloadAppDomainException
BadImageFormatException
DataMisalignedException
TypeLoadException
TypeUnloadedException
这些异常通常很难诊断,可能会揭示出库或调用它的代码中存在的难以理解的Bug。因此,和ApplicationException不同,把它们归为无法回答的情况是合理的。
实践:重新设计SqlException
请记住这些原则,让我们看下SqlException。除了网络错误(你根本无法到达服务器)外,在SQL Server的master.dbo.sysmessages表中有超过11000个不同的错误代码。因此,虽然该异常包含了你需要的所有底层信息,但是,除了简单地捕获&记录外,你实际上难以做任何事。
如果我们要重新设计SqlException,那么我们会希望,根据我们期望用户或开发人员做什么,将其分解成多个不同的类别。
SqlClient.NetworkException会表示所有说明数据库服务器本身之外的环境存在问题的错误代码。
SqlClient.InternalException enthält einen Fehlercode, der auf einen schwerwiegenden Fehler des Servers hinweist (z. B. Datenbankbeschädigung oder Unfähigkeit, auf die Festplatte zuzugreifen).
SqlClient.SyntaxException entspricht unserer ArgumentException. Das bedeutet, dass Sie fehlerhaftes SQL an den Server weitergeben (entweder direkt oder aufgrund eines ORM-Fehlers).
SqlClient.MissingObjectException tritt auf, wenn die Syntax korrekt ist, das Datenbankobjekt (Tabelle, Ansicht, gespeicherte Prozedur usw.) jedoch nicht vorhanden ist.
SqlClient.DeadlockException tritt auf, wenn zwei oder mehr Prozesse beim Versuch, dieselben Informationen zu ändern, in Konflikt geraten.
Jede dieser Anomalien impliziert eine Vorgehensweise.
SqlClient.NetworkException: Wiederholen Sie den Vorgang. Bei häufigerem Auftreten wenden Sie sich bitte an das Betriebs- und Wartungspersonal.
SqlClient.InternalException: Kontaktieren Sie sofort den DBA.
SqlClient.SyntaxException: Benachrichtigt den Anwendungs- oder Datenbankentwickler.
SqlClient.MissingObjectException: Bitten Sie das Betriebs- und Wartungspersonal zu überprüfen, ob bei der letzten Datenbankbereitstellung etwas verloren gegangen ist.
SqlClient.DeadlockException: Wiederholen Sie den Vorgang. Wenn es häufig vorkommt, suchen Sie nach Designfehlern.
Wenn wir dies in einem echten Job tun würden, müssten wir alle über 11.000 SQL Server-Fehlercodes einer dieser Kategorien zuordnen, was eine besonders entmutigende Aufgabe ist, die funktioniert erklärt, warum SqlException das ist, was es jetzt ist.
Zusammenfassung
Um die Problembehebung zu erleichtern, sollten beim Entwerfen einer API Ausnahmen entsprechend der Art der auszuführenden Aktion organisiert werden. Dies macht es einfacher, selbst korrigierten Code zu schreiben, genauere Protokolle zu führen und Probleme schneller an die richtige Person oder das richtige Team zu kommunizieren.
Über den Autor
Jonathan Allen begann Ende der 1990er Jahre, sich an MIS-Projekten für Arztpraxen zu beteiligen und steigerte diese schrittweise von Access und Excel auf die Unternehmensebene Lösung. Er verbrachte fünf Jahre damit, automatisierte Handelssysteme für die Finanzbranche zu programmieren, bevor er sich entschied, in die Entwicklung von High-End-Benutzeroberflächen einzusteigen. In seiner Freizeit studiert und schreibt er gerne über westliche Kampftechniken aus dem 15. bis 17. Jahrhundert.
Das Obige ist der Inhalt der .NET-Ausnahmedesignprinzipien. Weitere verwandte Inhalte finden Sie auf der chinesischen PHP-Website (www.php.cn)!