Attraper des exceptions avec "attraper, quand"

94

Je suis tombé sur cette nouvelle fonctionnalité en C # qui permet à un gestionnaire de capture de s'exécuter lorsqu'une condition spécifique est remplie.

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

J'essaie de comprendre quand cela peut être utile.

Un scénario pourrait être quelque chose comme ceci:

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

mais c'est encore quelque chose que je peux faire dans le même gestionnaire et déléguer à différentes méthodes en fonction du type de pilote. Cela rend-il le code plus facile à comprendre? Sans doute non.

Un autre scénario auquel je peux penser est quelque chose comme:

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

Encore une fois, c'est quelque chose que je peux faire comme:

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

L'utilisation de la fonctionnalité «catch, when» accélère-t-elle la gestion des exceptions car le gestionnaire est ignoré en tant que tel et le déroulement de la pile peut se produire beaucoup plus tôt que la gestion des cas d'utilisation spécifiques dans le gestionnaire? Y a-t-il des cas d'utilisation spécifiques qui correspondent mieux à cette fonctionnalité que les gens peuvent ensuite adopter comme bonne pratique?

MS Srikkanth
la source
8
C'est utile si le whenbesoin d'accéder à l'exception elle
Tim Schmelter
1
Mais c'est quelque chose que nous pouvons également faire dans le bloc du gestionnaire lui-même. Existe-t-il des avantages en dehors d'un «code légèrement plus organisé»?
MS Srikkanth
3
Mais alors vous avez déjà traité l'exception dont vous ne voulez pas. Et si vous voulez l'attraper ailleurs try..catch...catch..catch..finally?
Tim Schmelter
4
@ user3493289: Suite à cet argument, nous n'avons pas non plus besoin de vérifications automatiques de type dans les gestionnaires d'exceptions: nous ne pouvons qu'autoriser catch (Exception ex), vérifier le type et throwautrement. Un code légèrement plus organisé (évitant le bruit de code) est exactement la raison pour laquelle cette fonctionnalité existe. (C'est en fait vrai pour beaucoup de fonctionnalités.)
Heinzi
2
@TimSchmelter Merci. Postez-le comme réponse et je l'accepterai. Ainsi, le scénario réel serait alors «si la condition de traitement dépend de l'exception», alors utilisez cette fonctionnalité /
MS Srikkanth

Réponses:

118

Les blocs de capture vous permettent déjà de filtrer sur le type de l'exception:

catch (SomeSpecificExceptionType e) {...}

La whenclause vous permet d'étendre ce filtre aux expressions génériques.

Ainsi, vous utilisez la whenclause pour les cas où le type de l'exception n'est pas suffisamment distinct pour déterminer si l'exception doit être traitée ici ou non.


Un cas d'utilisation courant est celui des types d'exception qui sont en fait un wrapper pour plusieurs types d'erreurs différents.

Voici un cas que j'ai réellement utilisé (en VB, qui possède déjà cette fonctionnalité depuis un certain temps):

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

Idem pour SqlException, qui a aussi une ErrorCodepropriété. L'alternative serait quelque chose comme ça:

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

ce qui est sans doute moins élégant et casse légèrement la trace de la pile .

De plus, vous pouvez mentionner le même type d'exception deux fois dans le même bloc try-catch-block:

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

ce qui ne serait pas possible sans la whencondition.

Heinzi
la source
2
La deuxième approche ne permet pas non plus de l'attraper dans un autre catch, n'est-ce pas?
Tim Schmelter
@TimSchmelter. Vrai. Vous devrez gérer toutes les COMExceptions dans le même bloc.
Heinzi
Alors que le whenvous permet de gérer le même type d'exception plusieurs fois. Vous devriez le mentionner également car c'est une différence cruciale. Sans whencela, vous obtiendrez une erreur de compilation.
Tim Schmelter
1
En ce qui me concerne, la partie qui suit "En un mot:" devrait être la première ligne de la réponse.
CompuChip
1
@ user3493289: c'est souvent le cas avec du code laid. Vous pensez "Je ne devrais pas être dans ce désordre en premier lieu, refaire le code", et vous pensez aussi "il pourrait y avoir un moyen de soutenir ce design avec élégance, repenser le langage". Dans ce cas, il y a une sorte de seuil pour savoir à quel point vous voulez que votre ensemble de clauses catch soit laid, donc quelque chose qui rend certaines situations moins laides vous permet d'en faire plus dans votre seuil :-)
Steve Jessop
37

Du wiki de Roslyn (c'est moi qui souligne):

Les filtres d'exception sont préférables à la capture et au rejet, car ils laissent la pile indemne . Si l'exception plus tard provoque le vidage de la pile, vous pouvez voir d'où elle provient, plutôt que juste le dernier endroit où elle a été renvoyée.

C'est également une forme courante et acceptée d '«abus» d'utiliser des filtres d'exception pour les effets secondaires; par exemple, la journalisation. Ils peuvent inspecter une exception «survolant» sans intercepter sa route . Dans ces cas, le filtre sera souvent un appel à une fonction d'assistance à faux retour qui exécute les effets secondaires:

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

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

Le premier point mérite d'être démontré.

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

Si nous exécutons ceci dans WinDbg jusqu'à ce que l'exception soit atteinte, et imprimons la pile en utilisant, !clrstack -i -anous verrons juste le cadre 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')

Cependant, si nous changeons le programme à utiliser when:

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

Nous verrons que la pile contient également B le cadre 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')

Ces informations peuvent être très utiles lors du débogage des vidages sur incident.

Eli Arbel
la source
7
Cela me surprend. Ne laissera- t-il pas throw;(par opposition à throw ex;) la pile indemne également? +1 pour l'effet secondaire. Je ne suis pas sûr d'approuver cela, mais il est bon de connaître cette technique.
Heinzi
13
Ce n'est pas faux - cela ne fait pas référence à la trace de la pile - cela fait référence à la pile elle-même. Si vous regardez la pile dans un débogueur (WinDbg), et même si vous l'avez utilisée throw;, la pile se déroule et vous perdez les valeurs des paramètres.
Eli Arbel
1
Cela peut être extrêmement utile lors du débogage des vidages.
Eli Arbel
3
@Heinzi Voir ma réponse dans un autre fil de discussion où vous pouvez voir que cela throw;change un peu la trace de la pile et la throw ex;change beaucoup.
Jeppe Stig Nielsen
1
L'utilisation throwperturbe légèrement la trace de la pile. Les numéros de ligne sont différents lors de l'utilisation throwpar opposition à when.
Mike Zboray
7

Lorsqu'une exception est levée, la première passe de la gestion des exceptions identifie où l'exception sera interceptée avant de dérouler la pile; si / quand l'emplacement "catch" est identifié, tous les blocs "finalement" sont exécutés (notez que si une exception échappe à un bloc "finalement", le traitement de l'exception précédente peut être abandonné). Une fois que cela se produit, le code reprendra l'exécution à la "capture".

S'il y a un point d'arrêt dans une fonction qui est évalué dans le cadre d'un «quand», ce point d'arrêt suspendra l'exécution avant que tout déroulement de pile ne se produise; en revanche, un point d'arrêt à un "catch" ne suspendra l'exécution qu'après que tous les finallygestionnaires se soient exécutés.

Enfin, si les lignes 23 et 27 de l' fooappel bar, et l'appel sur la ligne 23 jette une exception qui est interceptée fooet renvoyée sur la ligne 57, alors la trace de pile suggérera que l'exception s'est produite lors de l'appel à barpartir de la ligne 57 [emplacement de la relance] , détruisant toute information indiquant si l'exception s'est produite dans l'appel de la ligne 23 ou de la ligne 27. Utiliser whenpour éviter d'attraper une exception évite en premier lieu une telle perturbation.

BTW, un modèle utile qui est gênant à la fois en C # et en VB.NET consiste à utiliser un appel de fonction dans une whenclause pour définir une variable qui peut être utilisée dans une finallyclause pour déterminer si la fonction s'est terminée normalement, pour gérer les cas où une fonction n'a aucun espoir de «résoudre» toute exception qui se produit, mais doit néanmoins prendre des mesures en fonction de celle-ci. Par exemple, si une exception est levée dans une méthode de fabrique qui est censée renvoyer un objet qui encapsule des ressources, toutes les ressources qui ont été acquises devront être libérées, mais l'exception sous-jacente doit percoler jusqu'à l'appelant. La façon la plus propre de gérer cela sémantiquement (mais pas syntaxiquement) est d'avoir unfinallybloquer vérifier si une exception s'est produite et, si tel est le cas, libérer toutes les ressources acquises au nom de l'objet qui ne sera plus renvoyé. Puisque le code de nettoyage n'a aucun espoir de résoudre la condition qui a causé l'exception, il ne devrait vraiment pas le catchfaire, mais a simplement besoin de savoir ce qui s'est passé. Appel d'une fonction comme:

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

dans une whenclause permettra à la fonction factory de savoir que quelque chose s'est passé.

supercat
la source