Catturare le eccezioni con "prendi, quando"


94

Mi sono imbattuto in questa nuova funzionalità in C # che consente l'esecuzione di un gestore di catch quando viene soddisfatta una condizione specifica.

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

Sto cercando di capire quando questo potrà mai essere utile.

Uno scenario potrebbe essere qualcosa del genere:

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

ma anche questo è qualcosa che posso fare all'interno dello stesso gestore e delegare a metodi diversi a seconda del tipo di driver. Questo rende il codice più facile da capire? Probabilmente no.

Un altro scenario a cui posso pensare è qualcosa del tipo:

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

Ancora una volta questo è qualcosa che posso fare come:

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

L'uso della funzione "prendi, quando" rende la gestione delle eccezioni più veloce perché il gestore viene saltato come tale e lo svolgimento dello stack può avvenire molto prima rispetto alla gestione dei casi d'uso specifici all'interno del gestore? Esistono casi d'uso specifici che si adattano meglio a questa funzione che le persone possono quindi adottare come buona pratica?


8
È utile se la whennecessità di accedere all'eccezione stessa
Tim Schmelter

1
Ma questo è qualcosa che possiamo fare anche all'interno del blocco gestore stesso. Ci sono vantaggi oltre a un "codice leggermente più organizzato"?
MS Srikkanth

3
Ma poi hai già gestito l'eccezione che non vuoi. E se volessi catturarlo da qualche altra parte in questo try..catch...catch..catch..finally?
Tim Schmelter

4
@ user3493289: seguendo questo argomento, non abbiamo bisogno di controlli automatici del tipo nei gestori di eccezioni: possiamo solo consentire catch (Exception ex), controllare il tipo e throwaltro. Il codice leggermente più organizzato (ovvero evitare il rumore del codice) è esattamente il motivo per cui esiste questa funzionalità. (Questo in realtà è vero per molte funzionalità.)
Heinzi

2
@TimSchmelter Grazie. Postalo come risposta e lo accetterò. Quindi lo scenario effettivo sarebbe quindi "se la condizione per la gestione dipende dall'eccezione", quindi utilizzare questa funzione /
MS Srikkanth

Risposte:


118

I blocchi di cattura consentono già di filtrare in base al tipo di eccezione:

catch (SomeSpecificExceptionType e) {...}

La whenclausola consente di estendere questo filtro a espressioni generiche.

Pertanto, si utilizza la whenclausola per i casi in cui il tipo di eccezione non è sufficientemente distinto per determinare se l'eccezione deve essere gestita qui o meno.


Un caso d'uso comune sono i tipi di eccezione che in realtà sono un wrapper per più tipi diversi di errori.

Ecco un caso che ho effettivamente utilizzato (in VB, che ha già questa funzione da un po 'di tempo):

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

Lo stesso SqlExceptionvale per , che ha anche una ErrorCodeproprietà. L'alternativa sarebbe qualcosa del genere:

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

che è probabilmente meno elegante e rompe leggermente la traccia dello stack .

Inoltre, puoi menzionare lo stesso tipo di eccezione due volte nello stesso blocco try-catch:

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

che non sarebbe possibile senza la whencondizione.


2
Anche il secondo approccio non permette di coglierlo in modo diverso catch, vero?
Tim Schmelter

@TimSchmelter. Vero. Dovresti gestire tutte le eccezioni COM nello stesso blocco.
Heinzi

Mentre whenti consente di gestire lo stesso tipo di eccezione più volte. Dovresti menzionarlo anche perché è una differenza cruciale. Senza whenotterrai un errore del compilatore.
Tim Schmelter

1
Per quanto mi riguarda, la parte che segue "In poche parole:" dovrebbe essere la prima riga della risposta.
CompuChip

1
@ user3493289: questo è spesso il caso del codice brutto però. Pensi "Non dovrei essere in questo pasticcio in primo luogo, ridisegnare il codice", e pensi anche che "potrebbe esserci un modo per supportare questo design elegantemente, ridisegnare il linguaggio". In questo caso c'è una sorta di soglia per quanto brutto vuoi che sia il tuo set di clausole di cattura, quindi qualcosa che rende meno brutte certe situazioni ti permette di fare di più entro la tua soglia :-)
Steve Jessop

37

Dal wiki di Roslyn (enfasi mia):

I filtri di eccezione sono preferibili alla cattura e al rilancio perché lasciano lo stack illeso . Se l'eccezione in seguito causa il dump dello stack, puoi vedere da dove proviene originariamente, piuttosto che solo l'ultimo punto in cui è stato rigettato.

È anche una forma comune e accettata di "abuso" utilizzare filtri di eccezione per gli effetti collaterali; es. registrazione. Possono ispezionare un'eccezione "in volo" senza intercettarne la rotta . In questi casi, il filtro sarà spesso una chiamata a una funzione di supporto di ritorno falso che esegue gli effetti collaterali:

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

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

Vale la pena dimostrare il primo punto.

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 eseguiamo questo in WinDbg fino a quando non viene colpita l'eccezione e stampiamo lo stack utilizzando !clrstack -i -a, vedremo solo il frame di 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')

Tuttavia, se cambiamo il programma per utilizzare when:

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

Vedremo che lo stack contiene anche Bil frame di:

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')

Queste informazioni possono essere molto utili durante il debug dei dump di arresto anomalo del sistema.


7
Questo mi sorprende. Non lascerà throw;(al contrario di throw ex;) anche lo stack illeso? +1 per la cosa degli effetti collaterali. Non sono sicuro di approvarlo, ma è bene conoscere quella tecnica.
Heinzi

13
Non è sbagliato - questo non si riferisce alla traccia dello stack - si riferisce allo stack stesso. Se guardi lo stack in un debugger (WinDbg), e anche se lo hai usato throw;, lo stack si srotola e perdi i valori dei parametri.
Eli Arbel

1
Questo può essere estremamente utile durante il debug dei dump.
Eli Arbel

3
@ Heinzi Vedi la mia risposta in un altro thread dove puoi vedere che throw;cambia leggermente la traccia dello stack e la throw ex;cambia molto.
Jeppe Stig Nielsen

1
L'utilizzo throwdisturba leggermente la traccia dello stack. I numeri di riga sono diversi quando vengono utilizzati throwrispetto a when.
Mike Zboray

7

Quando viene generata un'eccezione, il primo passaggio di gestione delle eccezioni identifica dove verrà rilevata l'eccezione prima di sbloccare lo stack; se / quando viene identificata la posizione "catch", vengono eseguiti tutti i blocchi "finalmente" (si noti che se un'eccezione sfugge a un blocco "finalmente", l'elaborazione dell'eccezione precedente potrebbe essere abbandonata). Una volta che ciò accade, il codice riprenderà l'esecuzione alla "cattura".

Se è presente un punto di interruzione all'interno di una funzione che viene valutata come parte di un "quando", tale punto di interruzione sospenderà l'esecuzione prima che si verifichi lo svolgimento dello stack; al contrario, un punto di interruzione in una "cattura" sospenderà l'esecuzione solo dopo che tutti i finallygestori sono stati eseguiti.

Infine, se le righe 23 e 27 della foochiamata bare la chiamata sulla riga 23 lancia un'eccezione che viene catturata fooe rilanciata sulla riga 57, allora lo stack trace suggerirà che l'eccezione si è verificata durante la chiamata bardalla riga 57 [posizione del rilancio] , distruggendo qualsiasi informazione sul fatto che l'eccezione si sia verificata nella chiamata della linea 23 o della linea 27. L'utilizzo whenper evitare di catturare un'eccezione in primo luogo evita tale disturbo.

A proposito, un modello utile che è fastidiosamente scomodo sia in C # che in VB.NET consiste nell'usare una chiamata di funzione all'interno di una whenclausola per impostare una variabile che può essere utilizzata all'interno di una finallyclausola per determinare se la funzione è stata completata normalmente, per gestire i casi in cui una funzione non ha alcuna speranza di "risolvere" le eccezioni che si verificano, ma deve comunque agire sulla base di esse. Ad esempio, se viene generata un'eccezione all'interno di un metodo factory che dovrebbe restituire un oggetto che incapsula le risorse, tutte le risorse acquisite dovranno essere rilasciate, ma l'eccezione sottostante dovrebbe filtrare fino al chiamante. Il modo più pulito per gestirlo semanticamente (anche se non sintatticamente) è avere un filefinallyblocco controlla se si è verificata un'eccezione e, in tal caso, rilascia tutte le risorse acquisite per conto dell'oggetto che non verrà più restituito. Poiché il codice di pulizia non ha alcuna speranza di risolvere qualunque condizione abbia causato l'eccezione, in realtà non dovrebbe catchfarlo, ma deve semplicemente sapere cosa è successo. Chiamare una funzione come:

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

all'interno di una whenclausola consentirà alla funzione di fabbrica di sapere che è successo qualcosa.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.