C# Language Filtri di eccezione


Esempio

I filtri di eccezione offrono agli sviluppatori la possibilità di aggiungere una condizione (sotto forma di espressione boolean ) a un blocco catch , consentendo l'esecuzione del catch solo se la condizione è true .

I filtri di eccezione consentono la propagazione delle informazioni di debug nell'eccezione originale, dove come utilizzando un'istruzione if all'interno di un blocco catch e il richiamo dell'eccezione interrompe la propagazione delle informazioni di debug nell'eccezione originale. Con i filtri delle eccezioni, l'eccezione continua a propagarsi verso l'alto nello stack di chiamate, a meno che la condizione non sia soddisfatta. Di conseguenza, i filtri delle eccezioni rendono l'esperienza di debug molto più semplice. Invece di fermarsi sull'istruzione throw , il debugger si fermerà sull'istruzione che lancia l'eccezione, mantenendo lo stato corrente e tutte le variabili locali. Le discariche sono coinvolte in modo simile.

I filtri di eccezione sono stati supportati dal CLR sin dall'inizio e sono stati accessibili da VB.NET e F # per oltre un decennio esponendo una parte del modello di gestione delle eccezioni CLR. Solo dopo il rilascio di C # 6.0 la funzionalità è stata disponibile anche per gli sviluppatori C #.


Utilizzo dei filtri delle eccezioni

I filtri di eccezione vengono utilizzati aggiungendo una clausola when all'espressione catch . È possibile usare qualsiasi espressione che restituisca un bool in una clausola when (tranne attendere ). La variabile Exception dichiarata ex è accessibile dall'interno della clausola when :

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

È possibile combinare più blocchi catch con clausole when . Il primo when clausola che restituisce true causerà l'intercettazione dell'eccezione. Il suo blocco di catch verrà inserito, mentre le altre clausole di catch verranno ignorate (le loro clausole when non saranno valutate). Per esempio:

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

Risky quando la clausola

Attenzione

Può essere rischioso utilizzare filtri di eccezione: quando l' Exception viene lanciata dall'interno del when la clausola, l' Exception dal when clausola viene ignorato e viene trattato come false . Questo approccio consente agli sviluppatori di scrivere when clausola senza occuparsi di casi non validi.

Il seguente esempio illustra un tale scenario:

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

Visualizza la demo

Si noti che i filtri delle eccezioni evitano i problemi con il numero di riga confusi associati all'uso di throw quando il codice fallito si trova all'interno della stessa funzione. Ad esempio in questo caso il numero di riga è riportato come 6 anziché 3:

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

Il numero di riga di eccezione è segnalato come 6 perché l'errore è stato rilevato e ri-generato con l'istruzione throw sulla riga 6.

Lo stesso non succede con i filtri delle eccezioni:

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

In questo esempio a è 0 quindi la clausola catch viene ignorata ma 3 viene riportato come numero di riga. Questo perché non si srotolano lo stack . Più specificamente, l'eccezione non viene rilevata sulla linea 5 perché a infatti fa uguale 0 e quindi non v'è alcuna possibilità per l'eccezione di essere ri-gettato sulla linea 6 perché la linea 6 non viene eseguito.


Registrazione come effetto collaterale

Le chiamate al metodo nella condizione possono causare effetti collaterali, pertanto i filtri delle eccezioni possono essere utilizzati per eseguire codice su eccezioni senza rilevarli. Un esempio comune che sfrutta questo è un metodo Log che restituisce sempre false . Ciò consente di tracciare le informazioni del registro durante il debug senza la necessità di ripetere l'eccezione.

Tieni presente che sebbene questo sembra essere un modo comodo di registrazione, può essere rischioso, specialmente se vengono utilizzati assembly di logging di terze parti. Questi potrebbero generare eccezioni durante l'accesso a situazioni non ovvie che potrebbero non essere rilevate facilmente (vedere Risky when(...) clausola precedente).

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

Visualizza la demo

L'approccio comune nelle versioni precedenti di C # era di registrare e rilanciare l'eccezione.

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

Visualizza la demo


Il blocco finally

Il blocco finally viene eseguito ogni volta che l'eccezione viene lanciata o meno. Una sottigliezza con le espressioni in when filtri delle eccezioni vengono eseguiti più in alto nello stack prima di entrare nei blocchi finally interni. Ciò può causare risultati e comportamenti imprevisti quando il codice tenta di modificare lo stato globale (come l'utente o la cultura del thread corrente) e reimpostarlo in un blocco finally .

Esempio: finally block

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

Uscita prodotta:

Inizio
Valutazioni: True
Infine interiore
Catturare
Finalmente esterno

Visualizza la demo

Nell'esempio precedente, se il metodo SomeOperation non desidera "perdere" lo stato globale cambia in clausole when del chiamante, dovrebbe anche contenere un blocco catch per modificare lo stato. Per esempio:

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

È anche comune vedere classi di helper IDisposable sfruttano la semantica dell'utilizzo di blocchi per raggiungere lo stesso obiettivo, poiché IDisposable.Dispose verrà sempre chiamato prima che un'eccezione chiamata all'interno di un blocco using inizi a scoppiare nello stack.