Captura de exceções com “catch, when”

94

Encontrei esse novo recurso em C # que permite que um manipulador catch seja executado quando uma condição específica for atendida.

int i = 0;
try
{
    throw new ArgumentNullException(nameof(i));
}
catch (ArgumentNullException e)
when (i == 1)
{
    Console.WriteLine("Caught Argument Null Exception");
}

Estou tentando entender quando isso pode ser útil.

Um cenário poderia ser mais ou menos assim:

try
{
    DatabaseUpdate()
}
catch (SQLException e)
when (driver == "MySQL")
{
    //MySQL specific error handling and wrapping up the exception
}
catch (SQLException e)
when (driver == "Oracle")
{
    //Oracle specific error handling and wrapping up of exception
}
..

mas isso é algo que posso fazer no mesmo manipulador e delegar a métodos diferentes dependendo do tipo de driver. Isso torna o código mais fácil de entender? Provavelmente não.

Outro cenário que posso imaginar é algo como:

try
{
    SomeOperation();
}
catch(SomeException e)
when (Condition == true)
{
    //some specific error handling that this layer can handle
}
catch (Exception e) //catchall
{
    throw;
}

Novamente, isso é algo que posso fazer como:

try
{
    SomeOperation();
}
catch(SomeException e)
{
    if (condition == true)
    {
        //some specific error handling that this layer can handle
    }
    else
        throw;
}

Usar o recurso 'catch, when' torna o tratamento de exceções mais rápido porque o manipulador é ignorado e o desenrolamento da pilha pode acontecer muito mais cedo quando comparado ao tratamento de casos de uso específicos dentro do manipulador? Existem casos de uso específicos que se adaptam melhor a esse recurso que as pessoas podem adotar como uma boa prática?

MS Srikkanth
fonte
8
É útil se a whennecessidade de acessar a própria exceção
Tim Schmelter
1
Mas isso é algo que também podemos fazer dentro do próprio bloco do manipulador. Há algum benefício além de um 'código um pouco mais organizado'?
MS Srikkanth
3
Mas então você já tratou da exceção que não deseja. E se você quiser pegá-lo em algum outro lugar try..catch...catch..catch..finally?
Tim Schmelter de
4
@ user3493289: Seguindo esse argumento, também não precisamos de verificações automáticas de tipo em tratadores de exceção: só podemos permitir catch (Exception ex), verificar o tipo e de throwoutra forma. Um código um pouco mais organizado (também conhecido como evitar o ruído do código) é exatamente por que esse recurso existe. (Isso é verdade para vários recursos.)
Heinzi
2
Obrigado @TimSchmelter. Poste como uma resposta e eu aceitarei. Portanto, o cenário real seria 'se a condição para tratamento depender da exceção', use este recurso /
MS Srikkanth

Respostas:

118

Os blocos de captura já permitem que você filtre o tipo de exceção:

catch (SomeSpecificExceptionType e) {...}

A whencláusula permite que você estenda este filtro para expressões genéricas.

Portanto, você usa a whencláusula para casos em que o tipo de exceção não é distinto o suficiente para determinar se a exceção deve ser tratada aqui ou não.


Um caso de uso comum são os tipos de exceção que, na verdade, são um invólucro para vários tipos de erros diferentes.

Aqui está um caso que eu realmente usei (em VB, que já tem esse recurso há algum tempo):

try
{
    SomeLegacyComOperation();
}
catch (COMException e) when (e.ErrorCode == 0x1234)
{
    // Handle the *specific* error I was expecting. 
}

Mesmo para SqlException, que também possui um ErrorCodeimóvel. A alternativa seria algo assim:

try
{
    SomeLegacyComOperation();
}
catch (COMException e)
{
    if (e.ErrorCode == 0x1234)
    {
        // Handle error
    }
    else
    {
        throw;
    }
}

que é indiscutivelmente menos elegante e quebra ligeiramente o rastreamento de pilha .

Além disso, você pode mencionar o mesmo tipo de exceção duas vezes no mesmo bloco try-catch:

try
{
    SomeLegacyComOperation();
}
catch (COMException e) when (e.ErrorCode == 0x1234)
{
    ...
}
catch (COMException e) when (e.ErrorCode == 0x5678)
{
    ...
}

o que não seria possível sem a whencondição.

Heinzi
fonte
2
A segunda abordagem também não permite pegar de uma forma diferente catch, não é?
Tim Schmelter
@TimSchmelter. Verdade. Você teria que lidar com todas as COMExceptions no mesmo bloco.
Heinzi
Enquanto o whenpermite que você trate o mesmo tipo de exceção várias vezes. Você também deve mencionar isso, pois é uma diferença crucial. Sem whenvocê obterá um erro do compilador.
Tim Schmelter
1
No que me diz respeito, a parte que segue "Em poucas palavras:" deve ser a primeira linha da resposta.
CompuChip
1
@ user3493289: este é frequentemente o caso com código feio. Você pensa "Eu não deveria estar nessa bagunça em primeiro lugar, redesenhar o código", e você também pensa "poderia haver uma maneira de apoiar esse design com elegância, redesenhar a linguagem". Neste caso, há uma espécie de limite para o quão feio você quer que seu conjunto de cláusulas catch seja, então algo que torna certas situações menos feias permite que você faça mais dentro do seu limite :-)
Steve Jessop
37

Do wiki de Roslyn (ênfase minha):

Filtros de exceção são preferíveis a capturar e relançar porque eles deixam a pilha ilesa . Se a exceção posteriormente fizer com que a pilha seja descarregada, você pode ver de onde ela veio originalmente, em vez de apenas o último lugar em que foi lançada novamente.

Também é uma forma comum e aceita de “abuso” usar filtros de exceção para efeitos colaterais; por exemplo, registro. Eles podem inspecionar uma exceção “voando” sem interceptar seu curso . Nesses casos, o filtro geralmente será uma chamada para uma função auxiliar de retorno falso que executa os efeitos colaterais:

private static bool Log(Exception e) { /* log it */ ; return false; }

 try {  } catch (Exception e) when (Log(e)) { }

O primeiro ponto vale a pena demonstrar.

static class Program
{
    static void Main(string[] args)
    {
        A(1);
    }

    private static void A(int i)
    {
        try
        {
            B(i + 1);
        }
        catch (Exception ex)
        {
            if (ex.Message != "!")
                Console.WriteLine(ex);
            else throw;
        }
    }

    private static void B(int i)
    {
        throw new Exception("!");
    }
}

Se executarmos isso no WinDbg até que a exceção seja atingida, e imprimirmos a pilha usando !clrstack -i -a, veremos apenas o quadro de A:

003eef10 00a7050d [DEFAULT] Void App.Program.A(I4)

PARAMETERS:
  + int i  = 1

LOCALS:
  + System.Exception ex @ 0x23e3178
  + (Error 0x80004005 retrieving local variable 'local_1')

No entanto, se mudarmos o programa para usar when:

catch (Exception ex) when (ex.Message != "!")
{
    Console.WriteLine(ex);
}

Veremos que a pilha também contém Bo quadro de:

001af2b4 01fb05aa [DEFAULT] Void App.Program.B(I4)

PARAMETERS:
  + int i  = 2

LOCALS: (none)

001af2c8 01fb04c1 [DEFAULT] Void App.Program.A(I4)

PARAMETERS:
  + int i  = 1

LOCALS:
  + System.Exception ex @ 0x2213178
  + (Error 0x80004005 retrieving local variable 'local_1')

Essas informações podem ser muito úteis ao depurar despejos de memória.

Eli Arbel
fonte
7
Isso me surpreende. Não vai throw;(ao invés de throw ex;) deixar a pilha ilesa também? 1 para o efeito colateral. Não tenho certeza se aprovo isso, mas é bom saber sobre essa técnica.
Heinzi
13
Não está errado - isso não se refere ao rastreamento da pilha - se refere à própria pilha. Se você olhar para a pilha em um depurador (WinDbg), e mesmo se você tiver usado throw;, a pilha se desenrola e você perde os valores dos parâmetros.
Eli Arbel
1
Isso pode ser extremamente útil ao depurar dumps.
Eli Arbel
3
@Heinzi Veja minha resposta em outro tópico onde você pode ver que throw;muda um pouco o rastreamento de pilha e throw ex;muito.
Jeppe Stig Nielsen
1
Usar throwperturba um pouco o rastreamento da pilha. Os números das linhas são diferentes quando usados throwem vez de when.
Mike Zboray
7

Quando uma exceção é lançada, a primeira passagem de tratamento de exceção identifica onde a exceção será capturada antes de desenrolar a pilha; se / quando o local "catch" é identificado, todos os blocos "finally" são executados (observe que se uma exceção escapar de um bloco "finally", o processamento da exceção anterior pode ser abandonado). Assim que isso acontecer, o código irá retomar a execução no "catch".

Se houver um ponto de interrupção dentro de uma função que é avaliada como parte de um "quando", esse ponto de interrupção suspenderá a execução antes que ocorra qualquer reversão da pilha; por outro lado, um ponto de interrupção em um "catch" só suspenderá a execução depois que todos os finallymanipuladores forem executados.

Finalmente, se as linhas 23 e 27 da foochamada bar, e a chamada na linha 23 lançar uma exceção que é capturada fooe relançada na linha 57, o rastreamento de pilha irá sugerir que a exceção ocorreu durante a chamada barda linha 57 [localização do relançamento] , destruindo qualquer informação sobre se a exceção ocorreu na chamada da linha 23 ou da linha 27. Usar whenpara evitar capturar uma exceção em primeiro lugar evita tal perturbação.

BTW, um padrão útil que é irritantemente estranho em C # e VB.NET é usar uma chamada de função dentro de uma whencláusula para definir uma variável que pode ser usada dentro de uma finallycláusula para determinar se a função foi concluída normalmente, para lidar com casos em que uma função não tem esperança de "resolver" qualquer exceção que ocorra, mas deve, no entanto, agir com base nela. Por exemplo, se uma exceção é lançada dentro de um método de fábrica que deve retornar um objeto que encapsula recursos, quaisquer recursos adquiridos precisarão ser liberados, mas a exceção subjacente deve chegar até o chamador. A maneira mais limpa de lidar com isso semanticamente (embora não sintaticamente) é ter umfinallybloco verificar se ocorreu uma exceção e, em caso afirmativo, libera todos os recursos adquiridos em nome do objeto que não será mais retornado. Como o código de limpeza não tem esperança de resolver qualquer condição que tenha causado a exceção, ele realmente não deveria catch, mas apenas precisa saber o que aconteceu. Chamando uma função como:

bool CopySecondArgumentToFirstAndReturnFalse<T>(ref T first, T second)
{
  first = second;
  return false;
}

dentro de um when cláusula permitirá que a função de fábrica saiba que algo aconteceu.

supergato
fonte