Partilhar via


Declarações de tratamento de exceções - throw, try-catch, try-finally, e try-catch-finally

Use as throw instruções e try para trabalhar com exceções. Use a throw instrução para lançar uma exceção. Use a instruçãotry execução de um bloco de código.

A referência da linguagem C# documenta a versão mais recentemente lançada da linguagem C#. Contém também documentação inicial para funcionalidades em pré-visualizações públicas para o próximo lançamento linguístico.

A documentação identifica qualquer funcionalidade introduzida pela primeira vez nas últimas três versões da língua ou em pré-visualizações públicas atuais.

Sugestão

Para saber quando uma funcionalidade foi introduzida pela primeira vez em C#, consulte o artigo sobre o histórico de versões da linguagem C#.

A throw declaração

A throw declaração lança uma exceção:

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

Em uma throw e; declaração, o resultado da expressão e deve ser implicitamente conversível em System.Exception.

Pode usar as classes de exceção incorporadas, como ArgumentOutOfRangeException ou InvalidOperationException. O .NET também fornece os seguintes métodos auxiliares para lançar exceções em determinadas condições: ArgumentNullException.ThrowIfNull e ArgumentException.ThrowIfNullOrEmpty. Você também pode definir suas próprias classes de exceção que derivam de System.Exception. Para obter mais informações, consulte Criando e lançando exceções.

Dentro de um catch bloco, use uma throw; instrução para relançar a exceção que o catch bloco trata:

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

Nota

throw; Preserva o rastreamento de pilha original da exceção, que é armazenado na Exception.StackTrace propriedade. Em contraste, throw e; atualiza a StackTrace propriedade de e.

Quando uma exceção é lançada, o Common Language Runtime (CLR) procura o catch bloco que pode lidar com essa exceção. Se o método atualmente executado não contiver esse catch bloco, o CLR examinará o método que chamou o método atual e assim por diante na pilha de chamadas. Se não houver bloco compatível catch , o CLR termina o thread em execução. Para obter mais informações, consulte a seção Como as exceções são tratadas da especificação da linguagem C#.

A throw expressão

Você também pode usar throw como uma expressão. Esta abordagem pode ser conveniente em vários casos, incluindo:

  • o operador condicional. O exemplo a seguir usa uma throw expressão para lançar um ArgumentException quando a matriz args passada está vazia:

    string first = args.Length >= 1 
        ? args[0]
        : throw new ArgumentException("Please supply at least one argument.");
    
  • o operador de coalescência nulo. O exemplo a seguir usa uma throw expressão para lançar um ArgumentNullException quando a cadeia de caracteres a ser atribuída a uma propriedade é null:

    public string Name
    {
        get => name;
        set => name = value ??
            throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null");
    }
    
  • um lambda ou método com corpo de expressão. O exemplo seguinte usa uma throw expressão para lançar um InvalidCastException e indicar que uma conversão para um DateTime valor não é suportada:

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

A try declaração

Pode usar a try instrução em qualquer uma das seguintes formas: try-catch - para tratar exceções que possam ocorrer durante a execução do código dentro de um try bloco, try-finally - para especificar o código que corre quando o controlo sai do try bloco, e try-catch-finally - como uma combinação das duas formas anteriores.

A try-catch declaração

Use a instrução para manipular exceções que podem ocorrer durante a try-catch execução de um bloco de código. Coloque o código onde uma exceção pode ocorrer dentro de um try bloco. Use uma cláusula catch para especificar o tipo base de exceções que você deseja manipular no bloco correspondente catch :

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

Pode fornecer várias cláusulas de captura:

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

Quando ocorre uma exceção, a verificação em tempo de execução verifica as cláusulas de captura na ordem especificada, de cima para baixo. No máximo, apenas um catch bloco é executado por qualquer exceção lançada. Como o exemplo anterior também mostra, você pode omitir a declaração de uma variável de exceção e especificar apenas o tipo de exceção em uma cláusula catch. Uma cláusula de captura sem qualquer tipo de exceção especificado corresponde a qualquer exceção e, se existir, deve ser a última cláusula de captura.

Para voltar a lançar uma exceção apanhada, use a throw afirmação, como mostra o exemplo seguinte:

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

Nota

throw; Preserva o rastreamento de pilha original da exceção, que é armazenado na Exception.StackTrace propriedade. Em contraste, throw e; atualiza a StackTrace propriedade de e.

Um filtro de when exceção

Junto com um tipo de exceção, você também pode especificar um filtro de exceção que examina ainda mais uma exceção e decide se o bloco correspondente catch manipula essa exceção. Um filtro de exceção é uma expressão booleana que segue a when palavra-chave, como mostra o exemplo a seguir:

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

O exemplo anterior usa um filtro de exceção para fornecer um único catch bloco para manipular exceções de dois tipos especificados.

Você pode fornecer várias catch cláusulas para o mesmo tipo de exceção se elas se distinguirem por filtros de exceção. Uma dessas cláusulas pode não ter filtro de exceção. Se essa cláusula existir, deve ser a última das cláusulas que especificam esse tipo de exceção.

Se uma catch cláusula tiver um filtro de exceção, ela poderá especificar o tipo de exceção que é igual ou menos derivado do que um tipo de exceção de uma catch cláusula que aparece depois dela. Por exemplo, se um filtro de exceção estiver presente, uma catch (Exception e) cláusula não precisa ser a última cláusula.

Filtros de exceção versus tratamento tradicional de exceções

Os filtros de exceção oferecem vantagens significativas em relação às abordagens tradicionais de tratamento de exceções. A principal diferença é quando a lógica de tratamento de exceções é avaliada:

  • Filtros de exceção (when): A expressão do filtro é avaliada antes que a pilha seja desenrolada. Isso significa que a pilha de chamadas original e todas as variáveis locais permanecem intactas durante a avaliação do filtro.
  • Blocos tradicionaiscatch: O bloco de captura corre depois de a pilha ser desenrolada, podendo perder informações valiosas de depuração.

Aqui está uma comparação mostrando a diferença:

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

Vantagens dos filtros de exceção

  • Melhor experiência de depuração: Como a pilha não é desenrolada até que um filtro corresponda, os depuradores podem mostrar o ponto original de falha com todas as variáveis locais intactas.
  • Benefícios de desempenho: Se nenhum filtro corresponder, a exceção continuará se propagando sem a sobrecarga de desenrolamento e restauração da pilha.
  • Código mais limpo: vários filtros podem lidar com diferentes condições do mesmo tipo de exceção sem exigir instruções if-else aninhadas.
  • Registro em log e diagnóstico: você pode examinar e registrar detalhes da exceção antes de decidir se deseja lidar com a exceção:
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");
}

Quando usar filtros de exceção

Use filtros de exceção quando precisar:

  • Lidar com exceções com base em condições ou propriedades específicas.
  • Preserve a pilha de chamadas original para depuração.
  • Registre ou examine as exceções antes de decidir se deseja tratá-las.
  • Manipule o mesmo tipo de exceção de forma diferente com base no contexto.
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");
}

Preservação de vestígios de pilha

Os filtros de exceção preservam a propriedade original ex.StackTrace . Se uma catch cláusula não conseguir processar a exceção e a voltar a lançar, a informação original da pilha perde-se. O when filtro não desenrola a pilha, portanto, se um when filtro for false, o rastreamento de pilha original não será alterado.

A abordagem do filtro de exceções é valiosa em aplicações onde preservar a informação de depuração é crucial para diagnosticar erros de código.

Exceções em métodos assíncronos e iteradores

Se uma exceção ocorrer numa função assíncrona, a exceção propaga-se para o chamador da função enquanto aguarda o resultado da função, como mostra o exemplo seguinte:

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

Se ocorrer uma exceção num método iterador, a exceção propaga-se para o chamador apenas quando este avança para o elemento seguinte.

A try-finally declaração

Numa try-finally instrução, o bloco finally executa-se quando o controlo sai do try bloco. O controle pode deixar o try bloco como resultado de

  • execução normal,
  • execução de uma instrução jump (ou seja, return, , breakcontinue, ou goto), ou
  • propagação de uma exceção fora do try bloco.

O exemplo a seguir usa o finally bloco para redefinir o estado de um objeto antes que o controle deixe o método:

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

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

Você também pode usar o finally bloco para limpar os recursos alocados try usados no bloco.

Nota

Quando o tipo de um recurso implementa a IDisposable interface ou IAsyncDisposable , considere a using instrução. A using declaração garante que os recursos adquiridos sejam descartados quando o controle deixar a using declaração. O compilador transforma uma using instrução em uma try-finally instrução.

Se o finally bloco é executado depende de o sistema operativo decidir desencadear uma operação de desmantelamento de exceções. Os únicos casos em que finally os blocos não são executados envolvem a terminação imediata de um programa. Por exemplo, tal rescisão pode acontecer por causa da Environment.FailFast chamada ou de uma OverflowExceptionInvalidProgramException ou exceção. A maioria dos sistemas operativos realiza uma limpeza razoável de recursos como parte da paragem e descarga do processo.

A try-catch-finally declaração

Use uma try-catch-finally instrução tanto para lidar com exceções que possam ocorrer durante a execução do try bloco como para especificar o código que deve ser executado quando o control sai da try instrução:

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

}

Quando um catch bloco trata uma exceção, o bloco finally é executado depois de o catch bloco terminar (mesmo que ocorra outra exceção durante a execução do catch bloco). Para obter informações sobre catch e finally blocos, consulte As seções A try-catch instrução e A try-finally instrução , respectivamente.

Especificação da linguagem C#

Para obter mais informações, consulte as seguintes seções da especificação da linguagem C#:

Consulte também