Exceptions abzufangen ist die eine Hälfte der Fehlerbehandlung; die andere ist, die richtige zu werfen. Eine gut gewählte Exception sagt dem Aufrufer genau, was schiefgelaufen ist und ob es sein Fehler war oder am Zustand des Programms liegt. Diese Seite behandelt die Seite des Werfens; try catch behandelt die Behandlung.
Die throw-Anweisung und Guard Clauses
throw nimmt ein Exception-Objekt. Die Ausführung der Methode stoppt dort, und die Exception sucht den Aufrufstapel hinauf nach einem Handler. Der häufigste Einsatz ist eine Guard Clause: Prüfungen am Anfang einer Methode, die ungültige Eingaben ablehnen, bevor irgendeine Arbeit erledigt wird.
Ausgabe:
ArgumentOutOfRangeException for parameter 'amount'
InvalidOperationException: The account is frozen.
Balance: 100
nameof(amount) erzeugt den String "amount" und bleibt korrekt, wenn der Parameter umbenannt wird. Die Argument-Exceptions speichern ihn in ParamName, das Tools und Logs verwenden, um auf das ungültige Argument zu zeigen.
Guard Clauses halten den Rest der Methode einfach: Nach den Prüfungen kann der Code von gültigen Eingaben ausgehen. Außerdem scheitern sie an der Stelle des Fehlers, statt einen ungültigen Wert weiterwandern zu lassen, der drei Methoden später eine verwirrende NullReferenceException verursacht.
Welchen Exception-Typ du wirfst
Nimm einen eingebauten Typ, wenn er die Situation beschreibt; Aufrufer wissen schon, wie sie ihn behandeln.
| Situation | Wirf |
|---|---|
Ein Pflichtargument ist null | ArgumentNullException |
| Ein Argument liegt außerhalb des erlaubten Bereichs (negative Menge, Index hinter dem Ende) | ArgumentOutOfRangeException |
| Ein Argument ist auf andere Weise ungültig (leerer Name, fehlerhafte ID) | ArgumentException |
| Der Aufruf ist im aktuellen Zustand des Objekts nicht gültig | InvalidOperationException |
Die Operation wird von diesem Typ nie unterstützt (das Add einer schreibgeschützten Collection) | NotSupportedException |
| Die Methode ist noch nicht geschrieben | NotImplementedException |
Ein Objekt wurde nach Dispose verwendet | ObjectDisposedException |
| Einer zeitgesteuerten Operation ist die Zeit ausgegangen | TimeoutException |
Die Grenze zwischen den ersten drei Zeilen und InvalidOperationException ist die Frage, wer etwas ändern muss. Eine Argument-Exception sagt „ruf das anders auf“. InvalidOperationException sagt „der Aufruf war in Ordnung, aber nicht jetzt“.
Wirf Exception, SystemException oder ApplicationException nicht direkt: Aufrufer können sie nicht abfangen, ohne auch alles andere abzufangen. Wirf auch NullReferenceException, IndexOutOfRangeException oder StackOverflowException nicht selbst; die Runtime reserviert sie für echte Bugs.
throw-Ausdrücke
Vor C# 7 war throw nur eine Anweisung. Seit C# 7 kann es an drei Stellen auch als Ausdruck vorkommen, was häufige Prüfungen zu einer Zeile macht:
Ausgabe:
Ana <ana@example.com>
Null: name
ArgumentException: email
Beachte, dass ArgumentNullException von ArgumentException ableitet, ein zuerst platziertes catch (ArgumentException) würde also auch den null-Fall abfangen. Aus demselben Grund zählt die Reihenfolge der catch-Klauseln, wenn du beide behandelst.
ThrowIfNull und Verwandte (ab .NET 6)
Modernes .NET bietet statische Hilfsmethoden, die Prüfung und Wurf für dich schreiben, wobei der Parametername automatisch erfasst wird:
public void Ship(Order order, int quantity, string address)
{
ArgumentNullException.ThrowIfNull(order); // .NET 6
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(quantity); // .NET 8
ArgumentException.ThrowIfNullOrWhiteSpace(address); // .NET 8
ObjectDisposedException.ThrowIf(disposed, this); // .NET 7
// ...
}
Sie verhalten sich wie das handgeschriebene if mit throw und halten Guard Clauses bei einer Zeile pro Prüfung. Bei älteren Zielen schreibe die weiter oben gezeigte Form mit if.
Eine eigene Exception-Klasse schreiben
Erzeuge einen eigenen Exception-Typ, wenn Aufrufer genau diesen Fehlschlag getrennt abfangen müssen oder wenn der Handler Daten braucht, die ein Meldungsstring nicht gut transportieren kann.
Ausgabe:
Cannot withdraw 25 from a balance of 15.
Short by 10
Die Konventionen:
- Der Name endet auf
Exception. - Sie leitet von
Exceptionab (oder von einem spezifischeren eingebauten Typ, wenn sie ein Sonderfall davon ist, etwaInvalidOperationException). - Sie hat die drei Standardkonstruktoren: keine Argumente, Meldung sowie Meldung plus innere Exception. Füge darüber hinaus eigene Konstruktoren hinzu.
- Zusätzliche Daten kommen in schreibgeschützte Properties, gesetzt im Konstruktor. Ein Handler kann dann auf
e.Requestedreagieren, statt die Meldung zu parsen.
Mit einer inneren Exception kapseln
Wenn ein Fehlschlag auf niedriger Ebene als einer auf höherer Ebene erscheinen soll, kapsle ihn. Das Original bleibt als InnerException erhalten, es geht also keine Information verloren:
Ausgabe:
Setting 'port' must be a number, got '80a'.
Caused by: FormatException
Der Aufrufer denkt jetzt in Begriffen der Konfiguration, die er versteht, und das Protokollieren von e.ToString() gibt die ganze Kette aus, einschließlich der FormatException und ihres Stacktraces. Kapsle nur, wenn du Bedeutung hinzufügst; jede Exception in eine allgemeine MyAppException zu kapseln lässt Handler nur durch InnerException graben.
Werfen gegenüber einem Ergebnis zurückgeben
Exceptions sind für Fehlschläge, die der Aufrufer im normalen Betrieb nicht erwartet. Für Ergebnisse, die Routine sind, etwa ein Nachschlagen, das oft nichts findet, oder Benutzereingaben, die oft ungültig sind, ist die .NET-Konvention das Try-Muster: bool zurückgeben und den Wert über einen out-Parameter liefern.
public bool TryWithdraw(decimal amount, out string error)
{
if (amount > Balance) { error = "Insufficient funds."; return false; }
Balance -= amount;
error = null;
return true;
}
Viele Typen bieten beides: int.Parse wirft, int.TryParse gibt false zurück; dict[key] wirft, dict.TryGetValue gibt false zurück. Eine Exception zu werfen kostet weit mehr, als einen Wert zurückzugeben, sie sollte also nicht auf einem Pfad liegen, der tausende Male pro Sekunde läuft. Zu out-Parametern siehe ref und out.
Gute Meldungen schreiben
Eine Exception-Meldung liest ein Entwickler, der ein Log ansieht. Sag darin, was falsch war, und nenne, wo es unbedenklich ist, den fehlerhaften Wert: „Quantity must be between 1 and 99, got 0.“ ist besser als „Invalid input.“. Schreibe ganze Sätze und halte Geheimnisse wie Passwörter und Tokens aus Meldungen heraus, denn sie landen in Logdateien.
Häufige Fehler
Exceptionselbst werfen. Aufrufer können sie nicht gezielt abfangen; nimm einen bestimmten Typ.- Die Meldung dort übergeben, wo der Parametername hingehört.
new ArgumentNullException("name")nimmt den Parameternamen; die Meldung kommt als zweites. - Fest eingetragene Parameternamen. Nimm
nameof(param), damit Umbenennungen sie korrekt halten. - Eigene Exceptions ohne zusätzliche Bedeutung. Wenn ein eingebauter Typ passt, nimm ihn.
- Den ursprünglichen Fehler beim Kapseln verlieren. Übergib ihn immer als innere Exception.
Häufig gestellte Fragen
Wie werfe ich in C# eine Exception?
Erzeuge ein Exception-Objekt und wirf es: throw new ArgumentException("Amount must be positive", nameof(amount));. Die Ausführung stoppt in dieser Zeile, und die Exception wandert den Aufrufstapel hinauf zum nächsten passenden catch. Wähle den spezifischsten eingebauten Typ, der das Problem beschreibt, oder einen eigenen Typ, wenn Aufrufer diesen Fall getrennt behandeln müssen.
Wie erstelle ich in C# eine eigene Exception?
Leite eine Klasse von Exception ab, deren Name auf Exception endet, und gib ihr die Standardkonstruktoren: einen ohne Argumente, einen mit einer Meldung und einen mit einer Meldung und einer inneren Exception, die jeweils den passenden Konstruktor base(...) aufrufen. Füge schreibgeschützte Properties für alle Daten hinzu, die ein Handler braucht, etwa eine Bestell-ID oder einen Kontostand.
Wann sollte ich ArgumentException und wann InvalidOperationException werfen?
Wirf eine ArgumentException (oder ArgumentNullException / ArgumentOutOfRangeException), wenn ein Aufrufer einen ungültigen Wert übergeben hat: Die Lösung ist, die Methode anders aufzurufen. Wirf InvalidOperationException, wenn die Argumente in Ordnung sind, das Objekt aber für den Aufruf im falschen Zustand ist, etwa beim Lesen aus einer geschlossenen Verbindung oder beim Abheben von einem gesperrten Konto.
Was ist ein throw-Ausdruck in C#?
Seit C# 7 kann throw an drei Stellen als Ausdruck verwendet werden: nach ??, als einer der beiden Zweige von ?: und als Rumpf eines Members oder Lambdas mit Ausdruckskörper. Zum Beispiel weist _name = name ?? throw new ArgumentNullException(nameof(name)); in einer Zeile zu oder wirft.
Was macht ArgumentNullException.ThrowIfNull?
Es ist eine statische Hilfsmethode aus .NET 6: ArgumentNullException.ThrowIfNull(customer); wirft ArgumentNullException mit automatisch eingetragenem Parameternamen, wenn customer null ist, und tut sonst nichts. Spätere Versionen haben ähnliche Hilfsmethoden wie ArgumentException.ThrowIfNullOrEmpty (.NET 7) und ArgumentOutOfRangeException.ThrowIfNegative (.NET 8) hinzugefügt.