Поделиться через


Операторы обработки исключений — throw, , try-catchtry-finallyиtry-catch-finally

Для работы с исключениями используются throw инструкции и try инструкции. Используйте инструкцию throw для создания исключения. Используйте инструкцию try для перехвата и обработки исключений, которые могут возникать во время выполнения блока кода.

Инструкция throw

Оператор throw создает исключение:

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

В операторе throw e; результат выражения e должен быть неявно преобразован в System.Exception.

Можно использовать встроенные классы исключений, например ArgumentOutOfRangeException или InvalidOperationException. .NET также предоставляет следующие вспомогательные методы для создания исключений в определенных условиях: ArgumentNullException.ThrowIfNull и ArgumentException.ThrowIfNullOrEmpty. Вы также можете определить собственные классы исключений, производные от System.Exception. Дополнительные сведения см. в разделе "Создание и создание исключений".

catch В блоке можно использовать инструкцию throw; для повторного создания исключения, которое обрабатывается блокомcatch:

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

Примечание.

throw; сохраняет исходную трассировку стека исключения, которая хранится в свойстве Exception.StackTrace . Напротив этого throw e; обновляется StackTrace свойство e.

При возникновении исключения среда CLR ищет catch блок , который может обрабатывать это исключение. Если текущий выполняемый метод не содержит такой catch блок, среда CLR смотрит на метод, который вызывает текущий метод, и т. д. в стеке вызовов. Если блок не catch найден, среда CLR завершает исполняемый поток. Дополнительные сведения см. в разделе "Обработка исключений" спецификации языка C#.

Выражение throw

Вы также можете использовать throw в качестве выражения. Это может быть удобно в ряде случаев, в том числе:

  • Условный оператор. В следующем примере используется выражение для созданияthrow, когда переданный ArgumentException массив args пуст:

    string first = args.Length >= 1 
        ? args[0]
        : throw new ArgumentException("Please supply at least one argument.");
    
  • Оператор объединения с NULL. В следующем примере используется throw выражение для создания ArgumentNullException строки для назначения свойству null:

    public string Name
    {
        get => name;
        set => name = value ??
            throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null");
    }
    
  • Метод или лямбда, воплощающие выражение. В следующем примере используется throw выражение для создания выражения InvalidCastException , указывающего, что преобразование в DateTime значение не поддерживается:

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

Инструкция try

Инструкцию try можно использовать в любой из следующих форм: try-catch для обработки исключений, которые могут возникать во время выполнения кода внутри try блока, try-finally — для указания кода, выполняемого при выходе try элемента управления из блока, и try-catch-finally в сочетании с предыдущими двумя формами.

Инструкция try-catch

Используйте инструкцию try-catch для обработки исключений, которые могут возникать во время выполнения блока кода. Поместите код, в котором может возникнуть исключение в блоке try . Используйте предложение catch, чтобы указать базовый тип исключений, которые необходимо обрабатывать в соответствующем catch блоке:

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

Можно указать несколько предложений 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.");
}

При возникновении исключения предложения catch проверяются в указанном порядке с верхней до нижней части. Не более одного catch блока выполняется только для любого вызываемого исключения. Как показано в предыдущем примере, можно опустить объявление переменной исключения и указать только тип исключения в предложении catch. Предложение catch без какого-либо указанного типа исключения соответствует любому исключению и, если оно присутствует, должно быть последним предложением catch.

Если вы хотите повторно создать исключение, используйте инструкциюthrow, как показано в следующем примере:

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

Примечание.

throw; сохраняет исходную трассировку стека исключения, которая хранится в свойстве Exception.StackTrace . Напротив этого throw e; обновляется StackTrace свойство e.

Фильтр исключений when

Наряду с типом исключения можно также указать фильтр исключений, который дополнительно проверяет исключение и решает, обрабатывает ли соответствующее catch исключение блок. Фильтр исключений — это логическое выражение, которое следует when ключевому слову, как показано в следующем примере:

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

В предыдущем примере фильтр исключений используется для предоставления одного catch блока для обработки исключений двух указанных типов.

Можно указать несколько catch предложений для одного типа исключения, если они различаются фильтрами исключений. Одно из этих предложений может не иметь фильтра исключений. Если такое предложение существует, оно должно быть последним из предложений, которые указывают этот тип исключения.

catch Если предложение имеет фильтр исключений, он может указать тип исключения, который совпадает с или менее производным, чем тип catch исключения предложения, который отображается после него. Например, если фильтр исключений присутствует, catch (Exception e) предложение не должно быть последним предложением.

Фильтры исключений и традиционная обработка исключений

Фильтры исключений обеспечивают значительные преимущества по сравнению с традиционными подходами к обработке исключений. Ключевое различие заключается в оценке логики обработки исключений:

  • Фильтры исключений (when): выражение фильтра вычисляется до отмены стека. Это означает, что исходный стек вызовов и все локальные переменные остаются неизменными во время оценки фильтра.
  • Традиционные catch блоки: блок catch выполняется после отмены стека, потенциально теряя ценные сведения об отладке.

Ниже приведено сравнение, показывающее разницу:

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

Преимущества фильтров исключений

  • Улучшенная отладка. Так как стек не будет расправлен до тех пор, пока фильтр не соответствует, отладчики могут показать исходную точку сбоя со всеми локальными переменными без изменений.
  • Преимущества производительности. Если фильтр не соответствует, исключение продолжает распространяться без дополнительных расходов на очистку и восстановление стека.
  • Более чистый код: несколько фильтров могут обрабатывать различные условия одного типа исключения, не требуя вложенных операторов if-else.
  • Ведение журнала и диагностика. Перед решением о том, следует ли обрабатывать исключение, можно изучить и просмотреть сведения об исключении.
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");
}

Когда следует использовать фильтры исключений

При необходимости используйте фильтры исключений:

  • Обработка исключений на основе определенных условий или свойств.
  • Сохраните исходный стек вызовов для отладки.
  • Журнал или проверка исключений перед решением о том, следует ли обрабатывать их.
  • Обрабатывать один и тот же тип исключения по-разному в зависимости от контекста.
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");
}

Сохранение трассировки стека

Фильтры исключений сохраняют исходное ex.StackTrace свойство. catch Если предложение не может обработать исключение и повторно создает исключение, исходные сведения стека будут потеряны. Фильтр when не отсоединяет стек, поэтому если when фильтр является false, исходная трассировка стека не изменяется.

Подход к фильтру исключений ценен в приложениях, где сохранение сведений об отладке имеет решающее значение для диагностики проблем.

Исключения в асинхронных и итераторах

Если исключение возникает в асинхронной функции, он распространяется на вызывающий объект функции при ожидании результата функции, как показано в следующем примере:

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

Если исключение возникает в методе итератора, оно распространяется на вызывающий объект, только если итератор переходит к следующему элементу.

Инструкция try-finally

В инструкции try-finallyfinally блок выполняется при выходе try элемента управления из блока. Элемент управления может оставить try блок в результате

  • нормальное выполнение,
  • выполнение инструкции перехода (т. е. , returnbreak, continueили ) или goto
  • распространение исключения из try блока.

В следующем примере блок используется finally для сброса состояния объекта перед выходом элемента управления из метода:

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

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

Вы также можете использовать finally блок для очистки выделенных ресурсов, используемых в блоке try .

Примечание.

Если тип ресурса реализует IDisposable или интерфейс, рассмотрите инструкциюIAsyncDisposableusing. Инструкция using гарантирует, что приобретенные ресурсы удаляются при выходе элемента using управления из оператора. Компилятор преобразует using оператор в try-finally оператор.

finally Выполнение блока зависит от того, выбирает ли операционная система активацию операции очистки исключения. Единственные случаи, когда finally блоки не выполняются, включают немедленное завершение программы. Например, такое завершение может произойти из-за Environment.FailFast вызова или OverflowExceptionInvalidProgramException исключения. Большинство операционных систем выполняют разумное очистку ресурсов в рамках остановки и выгрузки процесса.

Инструкция try-catch-finally

Оператор используется try-catch-finally как для обработки исключений, которые могут возникать во время выполнения try блока, так и для указания кода, который должен выполняться при выходе 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 , finally блок выполняется после выполнения этого catch блока (даже если во время выполнения catch блока возникает другое исключение). Сведения о инструкциях и catch блоках см. в разделах finally и try-catch соответственно.

Спецификация языка C#

Дополнительные сведения см. в следующих разделах статьи Спецификация языка C#:

См. также