C# Language Filtres d'exception


Exemple

Les filtres d'exception permettent aux développeurs d'ajouter une condition (sous la forme d'une expression boolean ) à un bloc catch , ce qui permet à catch d'exécuter uniquement si la condition est évaluée à true .

Les filtres d'exception permettent la propagation des informations de débogage dans l'exception d'origine, car en utilisant une instruction if dans un bloc catch et en relançant l'exception, la propagation des informations de débogage dans l'exception d'origine est interrompue. Avec les filtres d'exception, l'exception continue à se propager vers le haut dans la pile d'appels, sauf si la condition est remplie. Par conséquent, les filtres d'exception facilitent considérablement le débogage. Au lieu de s'arrêter sur l'instruction throw , le débogueur s'arrête sur l'instruction générant l'exception, avec l'état actuel et toutes les variables locales préservées. Les décharges accidentelles sont affectées de la même manière.

Les filtres d'exception sont pris en charge par le CLR depuis le début et ils sont accessibles depuis plus de dix ans depuis VB.NET et F # en exposant une partie du modèle de gestion des exceptions du CLR. Ce n'est qu'après la sortie de C # 6.0 que la fonctionnalité était également disponible pour les développeurs C #.


Utilisation de filtres d'exception

Les filtres d'exception sont utilisés en ajoutant une clause when à l'expression catch . Il est possible d'utiliser n'importe quelle expression renvoyant un bool dans une clause when (sauf wait ). La variable d'exception déclarée ex est accessible depuis la clause when :

var SqlErrorToIgnore = 123;
try
{
    DoSQLOperations();
}
catch (SqlException ex) when (ex.Number != SqlErrorToIgnore)
{
    throw new Exception("An error occurred accessing the database", ex);
}

Plusieurs blocs d' catch avec when clauses peuvent être combinées. La première when la clause de retour true entraînera l'exception à prendre. Son bloc catch sera entré, tandis que les autres clauses catch seront ignorées (leurs clauses when ne seront pas évaluées). Par exemple:

try
{ ... }
catch (Exception ex) when (someCondition) //If someCondition evaluates to true,
                                          //the rest of the catches are ignored.
{ ... }
catch (NotImplementedException ex) when (someMethod()) //someMethod() will only run if
                                                       //someCondition evaluates to false
{ ... }
catch(Exception ex) // If both when clauses evaluate to false
{ ... }

Article risqué quand

Mise en garde

Il peut être risqué d'utiliser des filtres d'exception: lorsqu'une Exception est générée à partir de la clause when , la clause Exception from the when est ignorée et traitée comme false . Cette approche permet aux développeurs d'écrire when la clause sans prendre en charge des cas invalides.

L'exemple suivant illustre un tel scénario:

public static void Main()
{
    int a = 7;
    int b = 0;
    try
    {
        DoSomethingThatMightFail();
    }
    catch (Exception ex) when (a / b == 0)
    {
        // This block is never reached because a / b throws an ignored
        // DivideByZeroException which is treated as false.
    }
    catch (Exception ex)
    {
        // This block is reached since the DivideByZeroException in the 
        // previous when clause is ignored.
    }
}

public static void DoSomethingThatMightFail()
{
    // This will always throw an ArgumentNullException.
    Type.GetType(null);
}

Voir la démo

Notez que les filtres d'exception évitent les problèmes de numéro de ligne déroutants associés à l'utilisation de la méthode throw lorsque le code défaillant fait partie de la même fonction. Par exemple, dans ce cas, le numéro de ligne est indiqué par 6 au lieu de 3:

1. int a = 0, b = 0;
2. try {
3.     int c = a / b;
4. }
5. catch (DivideByZeroException) {
6.     throw;
7. }

Le numéro de ligne d'exception est signalé par 6 car l'erreur a été interceptée et renvoyée avec l'instruction throw sur la ligne 6.

La même chose ne se produit pas avec les filtres d'exception:

1. int a = 0, b = 0;
2. try {
3.     int c = a / b;
4. }
5. catch (DivideByZeroException) when (a != 0) {
6.     throw;
7. }

Dans cet exemple, a est 0, alors la clause catch est ignorée mais 3 est signalée comme numéro de ligne. C'est parce qu'ils ne déroulent pas la pile . Plus précisément, l'exception n'est pas pris en ligne 5 car a fait effectivement égal 0 et donc il n'y a pas possibilité de l'exception à relancées sur la ligne 6 , car la ligne 6 n'exécute pas.


Enregistrement comme effet secondaire

Les appels de méthode dans la condition peuvent entraîner des effets secondaires, donc les filtres d'exception peuvent être utilisés pour exécuter du code sur des exceptions sans les intercepter. Un exemple courant qui en profite est une méthode Log qui renvoie toujours false . Cela permet de tracer les informations du journal lors du débogage sans avoir à relancer l'exception.

Sachez que même si cela semble être un moyen confortable de journalisation, cela peut être risqué, surtout si des assemblages de journalisation tiers sont utilisés. Celles-ci peuvent générer des exceptions lors de la connexion à des situations non évidentes qui peuvent ne pas être détectées facilement (voir la section Risque when(...) ci-dessus).

try
{
    DoSomethingThatMightFail(s);
}
catch (Exception ex) when (Log(ex, "An error occurred"))
{
    // This catch block will never be reached
}

// ...

static bool Log(Exception ex, string message, params object[] args)
{
    Debug.Print(message, args);
    return false;
}

Voir la démo

L'approche courante dans les versions précédentes de C # consistait à enregistrer et à renvoyer l'exception.

6,0
try
{
    DoSomethingThatMightFail(s);
}
catch (Exception ex)
{
     Log(ex, "An error occurred");
     throw;
}

// ...

static void Log(Exception ex, string message, params object[] args)
{
    Debug.Print(message, args);
}

Voir la démo


Le bloc finally

Le bloc finally s'exécute chaque fois que l'exception soit lancée ou non. Une subtilité avec des expressions dans when les filtres d'exception sont exécutés plus haut dans la pile avant d' entrer dans les blocs finally internes. Cela peut entraîner des résultats et des comportements inattendus lorsque le code tente de modifier l'état global (comme l'utilisateur ou la culture du thread en cours) et le restaurer dans un bloc finally .

Exemple: bloc finally

private static bool Flag = false;

static void Main(string[] args)
{
    Console.WriteLine("Start");
    try
    {
        SomeOperation();
    }
    catch (Exception) when (EvaluatesTo())
    {
        Console.WriteLine("Catch");
    }
    finally
    {
        Console.WriteLine("Outer Finally");
    }
}

private static bool EvaluatesTo()
{
    Console.WriteLine($"EvaluatesTo: {Flag}");
    return true;
}

private static void SomeOperation()
{
    try
    {
        Flag = true;
        throw new Exception("Boom");
    }
    finally
    {
        Flag = false;
        Console.WriteLine("Inner Finally");
    }
}

Sortie produite:

Début
Evaluates à: True
Intérieur enfin
Capture
Extérieur

Voir la démo

Dans l'exemple ci-dessus, si la méthode SomeOperation ne souhaite pas "modifier" l'état global des modifications apportées aux clauses when l'appelant, elle devrait également contenir un bloc catch pour modifier l'état. Par exemple:

private static void SomeOperation()
{
    try
    {
        Flag = true;
        throw new Exception("Boom");
    }
    catch
    {
       Flag = false;
       throw;
    }
    finally
    {
        Flag = false;
        Console.WriteLine("Inner Finally");
    }
}

Il est également courant de voir les classes d’aide IDisposable tirer parti de la sémantique de l’ utilisation de blocs pour atteindre le même objectif, car IDisposable.Dispose sera toujours appelé avant qu’une exception appelée dans un bloc using commence à se former dans la pile.