web-dev-qa-db-fra.com

Catching exceptions avec "catch, when"

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 pourrait ê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. Est-ce que cela rend 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 que le déroulement de la pile peut avoir lieu beaucoup plus tôt que lorsqu’il est comparé à la gestion des cas d’utilisation spécifiques dans le gestionnaire? Existe-t-il des cas d'utilisation spécifiques mieux adaptés à cette fonctionnalité que les utilisateurs peuvent ensuite adopter en tant que bonne pratique?

82
MS Srikkanth

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

catch (SomeSpecificExceptionType e) {...}

La clause when vous permet d’étendre ce filtre aux expressions génériques.

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


Un cas d'utilisation courant sont les types d'exception qui sont en réalité un wrapper pour plusieurs types d'erreur différents.

Voici un cas que j'ai effectivement 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 possède également une propriété ErrorCode. 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 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 condition when.

98
Heinzi

De Roslyn wiki (emphase le mien):

Les filtres d'exception sont préférables aux captures et aux renvois car ils laissent la pile indemne . Si, par la suite, l’exception provoque le vidage de la pile, vous pouvez voir d’où elle provenait à l’origine, plutôt que le seul endroit où elle a été relancée.

Il est également courant et accepté d’utiliser des filtres d’exception pour les effets secondaires. par exemple. enregistrement. Ils peuvent inspecter une exception "survolée" sans intercepter sa route . Dans ces cas, le filtre sera souvent un appel à une fonction d'assistance renvoyant des données erronées 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 frappée et imprimons la pile avec !clrstack -i -a, nous verrons uniquement 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 pour utiliser when:

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

Nous verrons que la pile contient également le cadre de B:

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.

35
Eli Arbel

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

S'il existe un point d'arrêt dans une fonction évaluée comme faisant partie d'un "quand", ce point d'arrêt suspendra l'exécution avant tout déroulement de la pile; en revanche, un point d'arrêt à un "arrêt" suspendra l'exécution uniquement après l'exécution de tous les gestionnaires finally.

Enfin, si les lignes 23 et 27 de foo appellent bar et que l'appel sur la ligne 23 lève une exception qui est interceptée dans foo et rediffusé à la ligne 57, la trace de pile suggérera que l'exception s'est produite lors de l'appel de bar à partir de la ligne 57 [emplacement du renvoi], détruisant toute information indiquant si l'exception s'est produite lors de l'appel de la ligne 23 ou de la ligne 27. Utiliser when pour éviter de capturer une exception en premier lieu évite une telle perturbation.

BTW, un modèle utile qui est ennuyeux en C # et VB.NET consiste à utiliser un appel de fonction dans une clause when pour définir une variable pouvant être utilisée dans une clause finally pour déterminer si le function terminée normalement, pour gérer les cas où une fonction n'a aucun espoir de "résoudre" une exception qui se produit mais qui doit néanmoins prendre des mesures en fonction de celle-ci. Par exemple, si une exception est générée dans une méthode d'usine censée renvoyer un objet qui encapsule des ressources, toutes les ressources acquises devront être libérées, mais l'exception sous-jacente devrait être transmise à l'appelant. Le moyen le plus propre de gérer sémantiquement (mais pas syntaxiquement) consiste à faire vérifier par un bloc finally si une exception s'est produite et, si tel est le cas, libérer toutes les ressources acquises pour le compte de l'objet qui ne sera plus renvoyé. Étant donné que le code de nettoyage n'a aucun espoir de résoudre la condition qui a provoqué l'exception, il ne devrait en aucun cas catch, mais doit simplement savoir ce qui s'est passé. Appeler une fonction comme:

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

dans une clause when, il sera possible pour la fonction usine de savoir que quelque chose s'est passé.

5
supercat