Ausnahmen mit "catch, when" abfangen

94

Ich bin auf diese neue Funktion in C # gestoßen, mit der ein Catch-Handler ausgeführt werden kann, wenn eine bestimmte Bedingung erfüllt ist.

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

Ich versuche zu verstehen, wann dies jemals nützlich sein kann.

Ein Szenario könnte ungefähr so ​​aussehen:

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

Aber dies ist wieder etwas, das ich innerhalb desselben Handlers tun und je nach Treibertyp an verschiedene Methoden delegieren kann. Ist der Code dadurch leichter zu verstehen? Wohl nein.

Ein anderes Szenario, an das ich denken kann, ist so etwas wie:

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

Auch dies ist etwas, was ich tun kann:

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

Verbessert die Verwendung der Funktion "catch, when" die Ausnahmebehandlung, da der Handler als solcher übersprungen wird und das Abwickeln des Stapels viel früher erfolgen kann als bei der Behandlung der spezifischen Anwendungsfälle innerhalb des Handlers? Gibt es spezielle Anwendungsfälle, die besser zu dieser Funktion passen und die dann als bewährte Methode übernommen werden können?

MS Srikkanth
quelle
8
Es ist nützlich, wenn die whenAusnahme selbst zugreifen muss
Tim Schmelter
1
Aber das ist etwas, was wir auch innerhalb des Handlerblocks selbst tun können. Gibt es Vorteile außer einem etwas besser organisierten Code?
MS Srikkanth
3
Aber dann haben Sie bereits die Ausnahme behandelt, die Sie nicht wollen. Was ist, wenn Sie es irgendwo anders darin fangen wollen try..catch...catch..catch..finally?
Tim Schmelter
4
@ user3493289: Nach diesem Argument benötigen wir auch keine automatischen Typprüfungen in Ausnahmebehandlungsroutinen: Wir können nur zulassen catch (Exception ex), den Typ prüfen und throwsonst. Etwas besser organisierter Code (auch bekannt als Vermeiden von Code-Rauschen) ist genau der Grund, warum diese Funktion vorhanden ist. (Dies gilt tatsächlich für viele Funktionen.)
Heinzi
2
@ TimSchmelter Danke. Poste es als Antwort und ich werde es akzeptieren. Das eigentliche Szenario wäre also "Wenn die Bedingung für die Behandlung von der Ausnahme abhängt", dann verwenden Sie diese Funktion /
MS Srikkanth

Antworten:

118

Mit Catch-Blöcken können Sie bereits nach dem Typ der Ausnahme filtern :

catch (SomeSpecificExceptionType e) {...}

Mit dieser whenKlausel können Sie diesen Filter auf generische Ausdrücke erweitern.

Daher verwenden Sie die whenKlausel für Fälle, in denen der Typ der Ausnahme nicht eindeutig genug ist, um zu bestimmen, ob die Ausnahme hier behandelt werden soll oder nicht.


Ein häufiger Anwendungsfall sind Ausnahmetypen, die tatsächlich ein Wrapper für mehrere verschiedene Arten von Fehlern sind.

Hier ist ein Fall, den ich tatsächlich verwendet habe (in VB, das diese Funktion bereits seit einiger Zeit bietet):

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

Gleiches gilt für SqlException , das auch eine ErrorCodeEigenschaft hat. Die Alternative wäre so etwas:

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

Das ist wohl weniger elegant und bricht leicht die Stapelspur .

Darüber hinaus können Sie das gleiche erwähnen Art ausnahmsweise zweimal im selben Try-Catch-Block:

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

was ohne die whenBedingung nicht möglich wäre .

Heinzi
quelle
2
Der zweite Ansatz erlaubt es auch nicht, es in einem anderen zu fangen catch, oder?
Tim Schmelter
@ TimSchmelter. Wahr. Sie müssten alle COMExceptions im selben Block behandeln.
Heinzi
Mit der whenOption können Sie denselben Ausnahmetyp mehrmals behandeln. Sie sollten das auch erwähnen, da es ein entscheidender Unterschied ist. Ohne erhalten whenSie einen Compilerfehler.
Tim Schmelter
1
Für mich sollte der Teil nach "Auf den Punkt gebracht:" die erste Zeile der Antwort sein.
CompuChip
1
@ user3493289: das ist aber oft bei hässlichem Code der Fall. Sie denken, "Ich sollte überhaupt nicht in diesem Chaos sein, den Code neu gestalten", und Sie denken auch, "es könnte eine Möglichkeit geben, dieses Design elegant zu unterstützen, die Sprache neu zu gestalten". In diesem Fall gibt es eine Art Schwelle dafür, wie hässlich Ihre Catch-Klauseln sein sollen. Wenn Sie also bestimmte Situationen weniger hässlich machen, können Sie innerhalb Ihrer Schwelle mehr erreichen :-)
Steve Jessop
37

Aus Roslyns Wiki (Schwerpunkt Mine):

Ausnahmefilter sind dem Fangen und erneuten Werfen vorzuziehen, da sie den Stapel unversehrt lassen . Wenn die Ausnahme später dazu führt, dass der Stapel ausgegeben wird, können Sie sehen, woher er ursprünglich stammt, und nicht nur an der letzten Stelle, an der er erneut geworfen wurde.

Es ist auch eine verbreitete und akzeptierte Form des „Missbrauchs“, Ausnahmefilter für Nebenwirkungen zu verwenden. zB Protokollierung. Sie können eine Ausnahme „vorbeifliegen“ untersuchen, ohne ihren Verlauf abzufangen . In diesen Fällen ist der Filter häufig ein Aufruf einer falsch zurückgegebenen Hilfsfunktion, die die folgenden Nebenwirkungen ausführt:

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

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

Der erste Punkt ist es wert, demonstriert zu werden.

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

Wenn wir dies in WinDbg ausführen, bis die Ausnahme getroffen wird, und den Stapel mit drucken, sehen !clrstack -i -awir nur den Rahmen von 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')

Wenn wir jedoch das zu verwendende Programm ändern when:

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

Wir werden sehen, dass der Stapel auch den Rahmen enthält 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')

Diese Informationen können beim Debuggen von Crash-Dumps sehr nützlich sein.

Eli Arbel
quelle
7
Das überrascht mich. Wird nicht throw;(im Gegensatz zu throw ex;) verlassen den unversehrt Stapel als auch? +1 für die Nebeneffektsache. Ich bin mir nicht sicher, ob ich damit einverstanden bin, aber es ist gut, über diese Technik Bescheid zu wissen.
Heinzi
13
Es ist nicht falsch - dies bezieht sich nicht auf den Stack - Trace - es auf den Stapel selbst bezieht. Wenn Sie sich den Stapel in einem Debugger (WinDbg) ansehen und selbst wenn Sie ihn verwendet haben throw;, wird der Stapel abgewickelt und Sie verlieren die Parameterwerte.
Eli Arbel
1
Dies kann beim Debuggen von Dumps äußerst nützlich sein.
Eli Arbel
3
@Heinzi Siehe meine Antwort in einem anderen Thread, in dem Sie sehen können, dass throw;sich die Stapelverfolgung ein wenig throw ex;ändert und sich stark ändert.
Jeppe Stig Nielsen
1
Die Verwendung throwstört die Stapelverfolgung geringfügig. Zeilennummern unterscheiden sich bei der Verwendung throwim Gegensatz zu when.
Mike Zboray
7

Wenn eine Ausnahme ausgelöst wird, gibt der erste Durchgang der Ausnahmebehandlung an, wo die Ausnahme zuvor abgefangen wird der Stapel abgewickelt wird. Wenn der Ort "catch" identifiziert wird, werden alle "finally" -Blöcke ausgeführt (beachten Sie, dass die Verarbeitung der früheren Ausnahme möglicherweise abgebrochen wird, wenn eine Ausnahme einem "finally" -Block entgeht. Sobald dies geschieht, setzt der Code die Ausführung am "catch" fort.

Wenn es innerhalb einer Funktion einen Haltepunkt gibt, der als Teil eines "Wann" ausgewertet wird, setzt dieser Haltepunkt die Ausführung aus, bevor ein Abwickeln des Stapels erfolgt. Im Gegensatz dazu wird ein Haltepunkt an einem "catch" die Ausführung schließlich nur aussetzenfinally Handler ausgeführt wurden.

Wenn schließlich die Zeilen 23 und 27 des fooAufrufs barund der Anruf in Zeile 23 eine Ausnahme fooauslösen, die in Zeile 57 abgefangen und erneut ausgelöst wird, deutet die Stapelverfolgung darauf hin, dass die Ausnahme beim Aufrufen barvon Zeile 57 [Ort des erneuten Auslösens] aufgetreten ist. und alle Informationen darüber zu zerstören, ob die Ausnahme beim Anruf in Leitung 23 oder 27 aufgetreten ist. Durch die Verwendung when, um zu vermeiden, dass eine Ausnahme abgefangen wird, werden solche Störungen vermieden.

Übrigens ist ein nützliches Muster, das sowohl in C # als auch in VB.NET ärgerlich umständlich ist, die Verwendung eines Funktionsaufrufs innerhalb einer whenKlausel, um eine Variable festzulegen, die innerhalb einer finallyKlausel verwendet werden kann, um zu bestimmen, ob die Funktion normal ausgeführt wurde, um Fälle zu behandeln, in denen eine Funktion vorliegt hat keine Hoffnung, eine auftretende Ausnahme zu "lösen", muss aber dennoch darauf basierende Maßnahmen ergreifen. Wenn beispielsweise eine Ausnahme innerhalb einer Factory-Methode ausgelöst wird, die ein Objekt zurückgeben soll, das Ressourcen kapselt, müssen alle erworbenen Ressourcen freigegeben werden. Die zugrunde liegende Ausnahme sollte jedoch bis zum Aufrufer versickern. Der sauberste Weg, dies semantisch (wenn auch nicht syntaktisch) zu handhaben, ist afinally Blockprüfung, ob eine Ausnahme aufgetreten ist, und in diesem Fall alle Ressourcen freizugeben, die für das Objekt erworben wurden, das nicht mehr benötigt wird zurück. Da der Bereinigungscode keine Hoffnung hat, die Bedingung zu beheben, die die Ausnahme verursacht hat, sollte er dies nicht catchtun, sondern muss lediglich wissen, was passiert ist. Aufruf einer Funktion wie:

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

Innerhalb einer whenKlausel kann die Factory-Funktion erkennen, dass etwas passiert ist.

Superkatze
quelle