Freigeben über


Ausnahmebehandlungsanweisungen: throw, try-catch, try-finally und try-catch-finally

Verwenden Sie die throw Anweisungen, try um mit Ausnahmen zu arbeiten. Die Anweisung throw wird zum Auslösen einer Ausnahme verwendet. Die Anweisung try wird verwendet, um Ausnahmen abzufangen und zu behandeln, die während der Ausführung eines Codeblocks auftreten können.

Die C#-Sprachreferenz dokumentiert die zuletzt veröffentlichte Version der C#-Sprache. Außerdem enthält sie erste Dokumentation für Features in der öffentlichen Vorschau für die kommende Sprachversion.

In der Dokumentation werden alle Features identifiziert, die in den letzten drei Versionen der Sprache oder in der aktuellen öffentlichen Vorschau eingeführt wurden.

Tipp

Informationen dazu, wann ein Feature erstmals in C# eingeführt wurde, finden Sie im Artikel zum Versionsverlauf der C#-Sprache.

Die Anweisung throw

Die Anweisung throw löst eine Ausnahme aus:

if (shapeAmount <= 0)
{
    throw new ArgumentOutOfRangeException(nameof(shapeAmount), "Amount of shapes must be positive.");
}

In einer throw e;-Anweisung muss das Ergebnis des Ausdrucks e implizit in System.Exception konvertierbar sein.

Sie können die integrierten Ausnahmeklassen verwenden, z ArgumentOutOfRangeException . B. oder InvalidOperationException. .NET bietet außerdem die folgenden Hilfsmethoden zum Auslösen von Ausnahmen unter bestimmten Bedingungen: ArgumentNullException.ThrowIfNull und ArgumentException.ThrowIfNullOrEmpty. Sie können auch eigene Ausnahmeklassen definieren, die von System.Exception abgeleitet werden. Weitere Informationen finden Sie unter Erstellen und Auslösen von Ausnahmen.

Verwenden Sie innerhalb eines catch Blocks eine throw; Anweisung, um die Ausnahme, die der catch Block behandelt, erneut auszuwerfen:

try
{
    ProcessShapes(shapeAmount);
}
catch (Exception e)
{
    LogError(e, "Shape processing failed.");
    throw;
}

Hinweis

throw; behält die ursprüngliche Stapelüberwachung der Ausnahme bei, die in der Exception.StackTrace-Eigenschaft gespeichert ist. Im Gegensatz dazu throw e; aktualisiert die StackTrace Eigenschaft von e.

Wenn eine Ausnahme ausgelöst wird, sucht die Common Language Runtime (CLR) nach dem catch-Block, der diese Ausnahme behandeln kann. Wenn die derzeit ausgeführte Methode keinen solchen catch-Block enthält, berücksichtigt die CLR die Methode, die die aktuelle Methode aufgerufen hat, dann die vorhergehende in der Aufrufliste usw. Wenn kein kompatibler catch Block vorhanden ist, beendet die CLR den ausgeführten Thread. Weitere Informationen finden Sie im Abschnitt Behandlung von Ausnahmen der C#-Sprachspezifikation.

Der throw-Ausdruck

Sie können auch throw als Ausdruck verwenden. Dieser Ansatz kann in mehreren Fällen praktisch sein, darunter:

  • Der bedingte Operator. Im folgenden Beispiel wird ein throw-Ausdruck verwendet, um eine ArgumentException auszulösen, wenn das übergebene args-Array leer ist:

    string first = args.Length >= 1 
        ? args[0]
        : throw new ArgumentException("Please supply at least one argument.");
    
  • Der NULL-Sammeloperator. Im folgenden Beispiel wird ein throw-Ausdruck verwendet, um eine ArgumentNullException auszulösen, wenn die Zeichenfolge, die einer Eigenschaft zugewiesen werden soll, null ist:

    public string Name
    {
        get => name;
        set => name = value ??
            throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null");
    }
    
  • Ein Ausdruckskörperlambda oder eine Ausdruckskörpermethode. Im folgenden Beispiel wird ein Ausdruck zum Auslösen eines throwInvalidCastException Ausdrucks verwendet, um anzugeben, dass eine Konvertierung in einen DateTime Wert nicht unterstützt wird:

    DateTime ToDateTime(IFormatProvider provider) =>
             throw new InvalidCastException("Conversion to a DateTime is not supported.");
    

Die Anweisung try

Sie können die try Anweisung in einer der folgenden Formen verwenden: try-catch - um Ausnahmen zu behandeln, die während der Ausführung des Codes innerhalb eines try Blocks auftreten können, - um den Code anzugeben, try-finally der ausgeführt wird, wenn das Steuerelement den try Block verlässt, und try-catch-finally - als Kombination der beiden vorherigen Formulare.

Die Anweisung try-catch

Die Anweisung try-catch wird verwendet, um Ausnahmen zu behandeln, die während der Ausführung eines Codeblocks auftreten können. Fügen Sie den Code, bei dem eine Ausnahme auftreten kann, in einen try-Block ein. Verwenden Sie eine Catch-Klausel, um den Basistyp der Ausnahmen anzugeben, die im entsprechenden catch-Block behandelt werden sollen:

try
{
    var result = Process(-3, 4);
    Console.WriteLine($"Processing succeeded: {result}");
}
catch (ArgumentException e)
{
    Console.WriteLine($"Processing failed: {e.Message}");
}

Sie können mehrere Catch-Klauseln angeben:

try
{
    var result = await ProcessAsync(-3, 4, cancellationToken);
    Console.WriteLine($"Processing succeeded: {result}");
}
catch (ArgumentException e)
{
    Console.WriteLine($"Processing failed: {e.Message}");
}
catch (OperationCanceledException)
{
    Console.WriteLine("Processing is cancelled.");
}

Wenn eine Ausnahme auftritt, überprüft die Laufzeit Catch-Klauseln in der angegebenen Reihenfolge von oben nach unten. Höchstens wird nur ein catch Block für jede ausgelöste Ausnahme ausgeführt. Wie im vorherigen Beispiel gezeigt, können Sie die Deklaration einer Ausnahmevariable weglassen und nur den Ausnahmetyp in einer Catch-Klausel angeben. Eine Catch-Klausel ohne angegebenen Ausnahmetyp entspricht jeder beliebigen Ausnahme und muss, falls vorhanden, die letzte Catch-Klausel sein.

Wenn Sie eine abgefangene Ausnahme erneut auslösen möchten, verwenden Sie die throw Anweisung, wie im folgenden Beispiel gezeigt:

try
{
    var result = Process(-3, 4);
    Console.WriteLine($"Processing succeeded: {result}");
}
catch (Exception e)
{
    LogError(e, "Processing failed.");
    throw;
}

Hinweis

throw; behält die ursprüngliche Stapelüberwachung der Ausnahme bei, die in der Exception.StackTrace-Eigenschaft gespeichert ist. Im Gegensatz dazu throw e; aktualisiert die StackTrace Eigenschaft von e.

Ein when-Ausnahmefilter

Zusammen mit einem Ausnahmetyp können Sie auch einen Ausnahmefilter angeben, der eine Ausnahme weiter untersucht und entscheidet, ob der entsprechende catch-Block diese Ausnahme behandelt. Ein Ausnahmefilter ist ein boolescher Ausdruck, der wie im folgenden Beispiel dem Schlüsselwort when folgt:

try
{
    var result = Process(-3, 4);
    Console.WriteLine($"Processing succeeded: {result}");
}
catch (Exception e) when (e is ArgumentException || e is DivideByZeroException)
{
    Console.WriteLine($"Processing failed: {e.Message}");
}

Im vorherigen Beispiel wird ein Ausnahmefilter verwendet, um einen einzelnen catch-Block anzugeben, mit dem Ausnahmen von zwei angegebenen Typen behandelt werden.

Sie können für einen Ausnahmetyp mehrere catch-Klauseln angeben, wenn sie sich durch Ausnahmefilter voneinander unterscheiden. Eine dieser Klauseln weist möglicherweise keinen Ausnahmefilter auf. Bei solch einer Klausel muss diese die letzte Klausel sein, die diesen Ausnahmetyp angibt.

Wenn eine catch-Klausel einen Ausnahmefilter enthält, kann sie den Ausnahmetyp angeben, der gleich oder weniger abgeleitet ist als ein Ausnahmetyp einer catch-Klausel, die danach angezeigt wird. Wenn beispielsweise ein Ausnahmefilter vorhanden ist, muss eine catch (Exception e)-Klausel nicht die letzte Klausel sein.

Ausnahmefilter im Vergleich zur herkömmlichen Ausnahmebehandlung

Ausnahmefilter bieten gegenüber herkömmlichen Ausnahmebehandlungsansätzen erhebliche Vorteile. Der Hauptunterschied besteht darin, dass die Ausnahmebehandlungslogik ausgewertet wird:

  • Ausnahmefilter (when): Der Filterausdruck wird ausgewertet , bevor der Stapel entladen wird. Dies bedeutet, dass der ursprüngliche Aufrufstapel und alle lokalen Variablen während der Filterauswertung intakt bleiben.
  • Herkömmliche catch Blöcke: Der Catch-Block wird ausgeführt, nachdem der Stapel entwundt wurde und potenziell wertvolle Debuginformationen verloren gehen.

Hier ist ein Vergleich, der den Unterschied zeigt:

public static void DemonstrateStackUnwindingDifference()
{
    var localVariable = "Important debugging info";
    
    try
    {
        ProcessWithExceptionFilter(localVariable);
    }
    catch (InvalidOperationException ex) when (ex.Message.Contains("filter"))
    {
        // Exception filter: Stack not unwound yet.
        // localVariable is still accessible in debugger.
        // Call stack shows original throwing location.
        Console.WriteLine($"Caught with filter: {ex.Message}");
        Console.WriteLine($"Local variable accessible: {localVariable}");
    }
    
    try
    {
        ProcessWithTraditionalCatch(localVariable);
    }
    catch (InvalidOperationException ex)
    {
        // Traditional catch: Stack already unwound.
        // Some debugging information may be lost.
        if (ex.Message.Contains("traditional"))
        {
            Console.WriteLine($"Caught with if: {ex.Message}");
            Console.WriteLine($"Local variable accessible: {localVariable}");
        }
        else
        {
            throw; // Re-throws and further modifies stack trace.
        }
    }
}

private static void ProcessWithExceptionFilter(string context)
{
    throw new InvalidOperationException($"Exception for filter demo: {context}");
}

private static void ProcessWithTraditionalCatch(string context)
{
    throw new InvalidOperationException($"Exception for traditional demo: {context}");
}

Vorteile von Ausnahmefiltern

  • Bessere Debugerfahrung: Da der Stapel erst nach übereinstimmungen eines Filters aufgewockt wird, können Debugger den ursprünglichen Fehlerpunkt mit allen lokalen Variablen anzeigen.
  • Leistungsvorteile: Wenn keine Filterübereinstimmungen auftreten, wird die Ausnahme weiterhin weitergegeben, ohne dass der Aufwand für die Stapelaussetzung und Wiederherstellung besteht.
  • Übersichtlicherer Code: Mehrere Filter können unterschiedliche Bedingungen desselben Ausnahmetyps verarbeiten, ohne dass verschachtelte if-else-Anweisungen erforderlich sind.
  • Protokollierung und Diagnose: Sie können Ausnahmedetails untersuchen und protokollieren, bevor Sie entscheiden, ob die Ausnahme behandelt werden soll:
public static void DemonstrateDebuggingAdvantage()
{
    var contextData = new Dictionary<string, object>
    {
        ["RequestId"] = Guid.NewGuid(),
        ["UserId"] = "user123",
        ["Timestamp"] = DateTime.Now
    };

    try
    {
        // Simulate a deep call stack.
        Level1Method(contextData);
    }
    catch (Exception ex) when (LogAndFilter(ex, contextData))
    {
        // This catch block may never execute if LogAndFilter returns false.
        // But LogAndFilter can examine the exception while the stack is intact.
        Console.WriteLine("Exception handled after logging");
    }
}

private static void Level1Method(Dictionary<string, object> context)
{
    Level2Method(context);
}

private static void Level2Method(Dictionary<string, object> context)
{
    Level3Method(context);
}

private static void Level3Method(Dictionary<string, object> context)
{
    throw new InvalidOperationException("Error in deep call stack");
}

private static bool LogAndFilter(Exception ex, Dictionary<string, object> context)
{
    // This method runs before stack unwinding.
    // Full call stack and local variables are still available.
    Console.WriteLine($"Exception occurred: {ex.Message}");
    Console.WriteLine($"Request ID: {context["RequestId"]}");
    Console.WriteLine($"Full stack trace preserved: {ex.StackTrace}");
    
    // Return true to handle the exception, false to continue search.
    return ex.Message.Contains("deep call stack");
}

Verwendung von Ausnahmefiltern

Verwenden Sie Ausnahmefilter, wenn Sie Folgendes benötigen:

  • Behandeln sie Ausnahmen basierend auf bestimmten Bedingungen oder Eigenschaften.
  • Bewahren Sie den ursprünglichen Aufrufstapel für das Debuggen auf.
  • Protokollieren oder untersuchen Sie Ausnahmen, bevor Sie entscheiden, ob sie behandelt werden sollen.
  • Behandeln Sie denselben Ausnahmetyp je nach Kontext unterschiedlich.
public static void HandleFileOperations(string filePath)
{
    try
    {
        // Simulate file operation that might fail.
        ProcessFile(filePath);
    }
    catch (IOException ex) when (ex.Message.Contains("access denied"))
    {
        Console.WriteLine("File access denied. Check permissions.");
    }
    catch (IOException ex) when (ex.Message.Contains("not found"))
    {
        Console.WriteLine("File not found. Verify the path.");
    }
    catch (IOException ex) when (IsNetworkPath(filePath))
    {
        Console.WriteLine($"Network file operation failed: {ex.Message}");
    }
    catch (IOException)
    {
        Console.WriteLine("Other I/O error occurred.");
    }
}

private static void ProcessFile(string filePath)
{
    // Simulate different types of file exceptions.
    if (filePath.Contains("denied"))
        throw new IOException("File access denied");
    if (filePath.Contains("missing"))
        throw new IOException("File not found");
    if (IsNetworkPath(filePath))
        throw new IOException("Network timeout occurred");
}

private static bool IsNetworkPath(string path)
{
    return path.StartsWith(@"\\") || path.StartsWith("http");
}

Erhaltung der Stapelablaufverfolgung

Ausnahmefilter behalten die ursprüngliche ex.StackTrace Eigenschaft bei. Wenn eine catch Klausel die Ausnahme nicht verarbeiten und erneut auslöst, gehen die ursprünglichen Stapelinformationen verloren. Der when Filter entspannt den Stapel nicht. Wenn ein when Filter lautet false, wird die ursprüngliche Stapelablaufverfolgung also nicht geändert.

Der Ausnahmefilteransatz ist in Anwendungen hilfreich, bei denen das Beibehalten von Debuginformationen für die Diagnose von Codefehlern von entscheidender Bedeutung ist.

Ausnahmen in asynchronen und Iteratormethoden

Wenn eine Ausnahme in einer asynchronen Funktion auftritt, wird die Ausnahme an den Aufrufer der Funktion weitergegeben, wenn Sie auf das Ergebnis der Funktion warten , wie im folgenden Beispiel gezeigt:

public static async Task Run()
{
    try
    {
        Task<int> processing = ProcessAsync(-1);
        Console.WriteLine("Launched processing.");

        int result = await processing;
        Console.WriteLine($"Result: {result}.");
    }
    catch (ArgumentException e)
    {
        Console.WriteLine($"Processing failed: {e.Message}");
    }
    // Output:
    // Launched processing.
    // Processing failed: Input must be non-negative. (Parameter 'input')
}

private static async Task<int> ProcessAsync(int input)
{
    if (input < 0)
    {
        throw new ArgumentOutOfRangeException(nameof(input), "Input must be non-negative.");
    }

    await Task.Delay(500);
    return input;
}

Wenn eine Ausnahme in einer Iteratormethode auftritt, wird die Ausnahme nur an den Aufrufer weitergegeben, wenn der Iterator zum nächsten Element wechselt.

Die Anweisung try-finally

In einer try-finally Anweisung wird der finally Block ausgeführt, wenn das Steuerelement den try Block verlässt. Die Steuerung kann den try-Block aus folgenden Gründen verlassen:

  • Normale Ausführung
  • Ausführung einer Jump-Anweisung (d. h., return, break, continue oder goto)
  • Weitergabe einer Ausnahme aus dem try-Block

Im folgenden Beispiel wird der finally-Block verwendet, um den Status eines Objekts zurückzusetzen, bevor die Steuerung die Methode verlässt:

public async Task HandleRequest(int itemId, CancellationToken ct)
{
    Busy = true;

    try
    {
        await ProcessAsync(itemId, ct);
    }
    finally
    {
        Busy = false;
    }
}

Sie können auch den finally-Block verwenden, um belegte Ressourcen, die im try-Block verwendet werden, zu bereinigen.

Hinweis

Wenn der Typ einer Ressource die Schnittstelle IDisposable oder IAsyncDisposable implementiert, sollten Sie die using-Anweisung berücksichtigen. Mit der using-Anweisung wird sichergestellt, dass abgerufene Ressourcen verworfen werden, wenn die Steuerung die using-Anweisung verlässt. Der Compiler transformiert eine using-Anweisung in eine try-finally-Anweisung.

Ob der finally Block ausgeführt wird, hängt davon ab, ob das Betriebssystem einen Ausnahmeentspannvorgang auslöst. Die einzigen Fälle, in denen finally Blöcke nicht ausgeführt werden, umfassen die sofortige Beendigung eines Programms. Eine solche Beendigung kann beispielsweise aufgrund des Environment.FailFast-Aufrufs oder einer OverflowException- oder InvalidProgramException-Ausnahme auftreten. Die meisten Betriebssysteme führen eine angemessene Ressourcenbereinigung im Rahmen des Anhaltens und Entladens des Prozesses durch.

Die Anweisung try-catch-finally

Verwenden Sie eine try-catch-finally Anweisung, um Ausnahmen zu behandeln, die während der Ausführung des try Blocks auftreten können, und um den Code anzugeben, der ausgeführt werden muss, wenn das Steuerelement die try Anweisung verlässt:

public async Task ProcessRequest(int itemId, CancellationToken ct)
{
    Busy = true;

    try
    {
        await ProcessAsync(itemId, ct);
    }
    catch (Exception e) when (e is not OperationCanceledException)
    {
        LogError(e, $"Failed to process request for item ID {itemId}.");
        throw;
    }
    finally
    {
        Busy = false;
    }

}

Wenn ein catch Block eine Ausnahme behandelt, wird der finally Block nach Abschluss des catch Blocks ausgeführt (auch wenn während der Ausführung des catch Blocks eine andere Ausnahme auftritt). Informationen zum catch-Block und zum finally-Block finden Sie in den Abschnitten Die try-catch-Anweisung bzw. Die try-finally-Anweisung.

C#-Sprachspezifikation

Weitere Informationen finden Sie in den folgenden Abschnitten der C#-Sprachspezifikation:

Weitere Informationen