Udostępnij przez


Instrukcje obsługi wyjątków — throw, try-catch, try-finallyi try-catch-finally

Użyj instrukcji throw i try , aby pracować z wyjątkami. Użyj instrukcjithrow aby zgłosić wyjątek. Użyj instrukcjitry aby przechwycić wyjątki, które mogą wystąpić podczas wykonywania bloku kodu i obsługiwać je.

Dokumentacja języka C# zawiera ostatnio wydaną wersję języka C#. Zawiera również początkową dokumentację funkcji w publicznej wersji zapoznawczej nadchodzącej wersji językowej.

Dokumentacja identyfikuje dowolną funkcję po raz pierwszy wprowadzoną w ostatnich trzech wersjach języka lub w bieżącej publicznej wersji zapoznawczej.

Wskazówka

Aby dowiedzieć się, kiedy funkcja została po raz pierwszy wprowadzona w języku C#, zapoznaj się z artykułem dotyczącym historii wersji języka C#.

Instrukcja throw

Instrukcja throw zgłasza wyjątek:

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

throw e; W instrukcji wynik wyrażenia e musi być niejawnie konwertowany na System.Exception.

Można użyć wbudowanych klas wyjątków, takich jak ArgumentOutOfRangeException lub InvalidOperationException. Platforma .NET udostępnia również następujące metody pomocnicze do zgłaszania wyjątków w określonych warunkach: ArgumentNullException.ThrowIfNull i ArgumentException.ThrowIfNullOrEmpty. Można również zdefiniować własne klasy wyjątków pochodzące z klasy System.Exception. Aby uzyskać więcej informacji, zobacz Tworzenie i zgłaszanie wyjątków.

catch Wewnątrz bloku użyj throw; instrukcji , aby ponownie zgłosić wyjątek obsługiwany przez catch blok:

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

Uwaga

throw; Zachowuje oryginalny ślad stosu wyjątku, który jest przechowywany we Exception.StackTrace właściwości . Z kolei throw e; aktualizuje StackTrace właściwość e.

Po wystąpieniu wyjątku środowisko uruchomieniowe języka wspólnego (CLR) wyszukuje catch blok , który może obsłużyć ten wyjątek. Jeśli aktualnie wykonywana metoda nie zawiera takiego bloku, CLR analizuje metodę catch , która nazwała bieżącą metodę, i tak dalej w górę stosu wywołań. Jeśli nie ma zgodnego catch bloku, clR kończy wykonywanie wątku. Aby uzyskać więcej informacji, zobacz sekcję How exceptions are handled (Jak są obsługiwane wyjątki) specyfikacji języka C#.

Wyrażenie throw

Można również użyć throw jako wyrażenia. Takie podejście może być wygodne w kilku przypadkach, w tym:

  • operator warunkowy. W poniższym przykładzie użyto throw wyrażenia , aby zgłosić, ArgumentException gdy przekazana tablica args jest pusta:

    string first = args.Length >= 1 
        ? args[0]
        : throw new ArgumentException("Please supply at least one argument.");
    
  • operator łączenia wartości null. W poniższym przykładzie użyto wyrażenia , throw aby zgłosić ArgumentNullException , kiedy ciąg do przypisania do właściwości to null:

    public string Name
    {
        get => name;
        set => name = value ??
            throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null");
    }
    
  • wyrażenie-bodied lambda lub metoda. W poniższym przykładzie użyto wyrażenia , throw aby zgłosić wartość InvalidCastException , aby wskazać, że konwersja na DateTime wartość nie jest obsługiwana:

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

Instrukcja try

Instrukcję try można użyć w dowolnej z następujących form: try-catch — do obsługi wyjątków, które mogą wystąpić podczas wykonywania kodu wewnątrz try bloku, try-finally — w celu określenia kodu uruchamianego podczas opuszczania try bloku i try-catch-finally — jako kombinacji poprzednich dwóch formularzy.

Instrukcja try-catch

Użyj instrukcji try-catch , aby obsługiwać wyjątki, które mogą wystąpić podczas wykonywania bloku kodu. Umieść kod, w którym może wystąpić wyjątek wewnątrz try bloku. Użyj klauzuli catch, aby określić podstawowy typ wyjątków, które mają być obsługiwane w odpowiednim catch bloku:

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

Możesz podać kilka klauzul catch:

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.");
}

W przypadku wystąpienia wyjątku środowisko uruchomieniowe sprawdza klauzule catch w określonej kolejności od góry do dołu. Co najwyżej jeden catch blok jest uruchamiany dla każdego zgłaszanego wyjątku. Jak pokazano w poprzednim przykładzie, można pominąć deklarację zmiennej wyjątku i określić tylko typ wyjątku w klauzuli catch. Klauzula catch bez żadnego określonego typu wyjątku pasuje do wyjątku i, jeśli istnieje, musi być ostatnią klauzulą catch.

Aby ponownie zgłosić wyjątek przechwycony, użyj instrukcjithrow , jak pokazano w poniższym przykładzie:

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

Uwaga

throw; Zachowuje oryginalny ślad stosu wyjątku, który jest przechowywany we Exception.StackTrace właściwości . Z kolei throw e; aktualizuje StackTrace właściwość e.

when Filtr wyjątku

Oprócz typu wyjątku można również określić filtr wyjątku, który dodatkowo analizuje wyjątek i decyduje, czy odpowiedni catch blok obsługuje ten wyjątek. Filtr wyjątku jest wyrażeniem logicznym, które jest zgodne ze when słowem kluczowym, jak pokazano w poniższym przykładzie:

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}");
}

W poprzednim przykładzie użyto filtru wyjątku w celu zapewnienia pojedynczego catch bloku do obsługi wyjątków dwóch określonych typów.

Można podać kilka catch klauzul dla tego samego typu wyjątku, jeśli rozróżniają one filtry wyjątków. Jedna z tych klauzul może nie mieć filtru wyjątków. Jeśli taka klauzula istnieje, musi to być ostatnia z klauzul określających ten typ wyjątku.

Jeśli klauzula catch ma filtr wyjątku, może określić typ wyjątku, który jest taki sam, jak lub mniej pochodny niż typ wyjątku klauzuli catch , która pojawia się po nim. Jeśli na przykład istnieje filtr wyjątku, klauzula catch (Exception e) nie musi być ostatnią klauzulą.

Filtry wyjątków a tradycyjna obsługa wyjątków

Filtry wyjątków zapewniają znaczne korzyści w porównaniu z tradycyjnymi metodami obsługi wyjątków. Kluczową różnicą jest ocena logiki obsługi wyjątków:

  • Filtry wyjątków (when): wyrażenie filtru jest oceniane przed odłączeniu stosu. Oznacza to, że oryginalny stos wywołań i wszystkie zmienne lokalne pozostają nienaruszone podczas oceny filtru.
  • Tradycyjne catch bloki: blok catch jest uruchamiany po odłączeniu stosu, co może spowodować utratę cennych informacji debugowania.

Oto porównanie pokazujące różnicę:

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}");
}

Zalety filtrów wyjątków

  • Lepsze środowisko debugowania: ponieważ stos nie jest wywoływany do momentu dopasowania filtru, debugery mogą wyświetlać oryginalny punkt awarii ze wszystkimi zmiennymi lokalnymi bez zmian.
  • Korzyści z wydajności: jeśli nie ma pasujących filtrów, wyjątek będzie nadal propagowany bez konieczności odwijania i przywracania stosu.
  • Kod czyszczący: Wiele filtrów może obsługiwać różne warunki tego samego typu wyjątku bez konieczności zagnieżdżenia instrukcji if-else.
  • Rejestrowanie i diagnostyka: Przed podjęciem decyzji o tym, czy należy obsłużyć wyjątek, możesz sprawdzić szczegóły wyjątku i rejestrować je:
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");
}

Kiedy należy używać filtrów wyjątków

Użyj filtrów wyjątków, jeśli musisz:

  • Obsługa wyjątków na podstawie określonych warunków lub właściwości.
  • Zachowaj oryginalny stos wywołań na potrzeby debugowania.
  • Rejestruj lub sprawdzaj wyjątki przed podjęciem decyzji, czy je obsługiwać.
  • Obsłuż ten sam typ wyjątku inaczej na podstawie kontekstu.
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");
}

Zachowywanie śledzenia stosu

Filtry wyjątków zachowują oryginalną ex.StackTrace właściwość. Jeśli klauzula catch nie może przetworzyć wyjątku i zgłosi go ponownie, oryginalne informacje o stosie zostaną utracone. Filtr when nie odwija stosu, więc jeśli when filtr to false, oryginalny ślad stosu nie zostanie zmieniony.

Metoda filtrowania wyjątków jest cenna w aplikacjach, w których zachowanie informacji debugowania ma kluczowe znaczenie dla diagnozowania błędów kodu.

Wyjątki w metodach asynchronicznych i iteratorowych

Jeśli wyjątek występuje w funkcji asynchronicznych, wyjątek jest propagowany do obiektu wywołującego funkcji, gdy oczekujesz na wynik funkcji, jak pokazano w poniższym przykładzie:

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;
}

Jeśli wyjątek występuje w metodzie iteratora, wyjątek jest propagowany do obiektu wywołującego tylko wtedy, gdy iterator przechodzi do następnego elementu.

Instrukcja try-finally

try-finally W instrukcji blok jest uruchamiany, gdy kontrolka finallytry opuszcza blok. Kontrolka try może pozostawić blok w wyniku

  • normalne wykonywanie,
  • wykonywanie instrukcji skoku
  • propagacja wyjątku z try bloku.

W poniższym przykładzie użyto finally bloku do zresetowania stanu obiektu przed opuszczeniem metody przez kontrolkę:

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

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

Blok umożliwia również finally wyczyszczenie przydzielonych zasobów używanych try w bloku.

Uwaga

Jeśli typ zasobu implementuje interfejs lub, należy wziąć pod uwagę instrukcję IDisposableIAsyncDisposable .using Instrukcja using gwarantuje, że pozyskane zasoby zostaną usunięte, gdy kontrolka opuści instrukcję using . Kompilator przekształca instrukcję using w instrukcję try-finally .

finally To, czy blok jest wykonywany, zależy od tego, czy system operacyjny zdecyduje się wyzwolić operację odwijającego wyjątku. Jedynymi przypadkami, w których finally bloki nie są wykonywane, są natychmiastowe zakończenie programu. Na przykład takie zakończenie może wystąpić z Environment.FailFast powodu wywołania lub OverflowException wyjątku.InvalidProgramException Większość systemów operacyjnych wykonuje rozsądne czyszczenie zasobów w ramach zatrzymywania i zwalniania procesu.

Instrukcja try-catch-finally

try-catch-finally Użyj instrukcji , aby obsługiwać wyjątki, które mogą wystąpić podczas wykonywania try bloku i określić kod, który musi być uruchamiany, gdy kontrolka opuszcza instrukcję try :

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;
    }

}

catch Gdy blok obsługuje wyjątek, finally blok jest uruchamiany po zakończeniu catch bloku (nawet jeśli podczas wykonywania catch bloku wystąpi inny wyjątek). Aby uzyskać informacje na temat catch i bloków, zobacz finallytry-catch odpowiednio instrukcje i .try-finally

specyfikacja języka C#

Aby uzyskać więcej informacji, zobacz następujące sekcje specyfikacji języka C#:

Zobacz też