Łapanie wyjątków ze słowem „złap, kiedy”

97

Natknąłem się na tę nową funkcję w C #, która pozwala programowi obsługi catch na wykonanie, gdy zostanie spełniony określony warunek.

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

Próbuję zrozumieć, kiedy może się to kiedykolwiek przydać.

Jeden scenariusz może wyglądać mniej więcej tak:

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

ale jest to znowu coś, co mogę zrobić w ramach tego samego programu obsługi i delegować na różne metody w zależności od typu sterownika. Czy to sprawia, że ​​kod jest łatwiejszy do zrozumienia? Prawdopodobnie nie.

Inny scenariusz, który przychodzi mi do głowy, to coś takiego:

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

Znowu to jest coś, co mogę zrobić:

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

Czy użycie funkcji „catch, when” przyspiesza obsługę wyjątków, ponieważ program obsługi jest pomijany jako taki, a rozwijanie stosu może nastąpić znacznie wcześniej niż w porównaniu z obsługą określonych przypadków użycia w ramach procedury obsługi? Czy są jakieś konkretne przypadki użycia, które lepiej pasują do tej funkcji i które ludzie mogą następnie przyjąć jako dobrą praktykę?

MS Srikkanth
źródło
9
Jest to przydatne, jeśli whentrzeba uzyskać dostęp do samego wyjątku
Tim Schmelter
1
Ale to jest coś, co możemy zrobić również w samym bloku obsługi. Czy oprócz „nieco bardziej zorganizowanego kodu” są jakieś korzyści?
MS Srikkanth,
3
Ale wtedy już obsłużyłeś wyjątek, którego nie chcesz. A jeśli chcesz to złapać gdzie indziej w tym try..catch...catch..catch..finally?
Tim Schmelter,
4
@ user3493289: Idąc za tym argumentem, nie potrzebujemy również automatycznego sprawdzania typu w programach obsługi wyjątków: możemy tylko zezwalać catch (Exception ex), sprawdzać typ i throwinne. Nieco bardziej zorganizowany kod (czyli unikanie szumu kodu) jest dokładnie powodem, dla którego istnieje ta funkcja. (W rzeczywistości jest to prawdą dla wielu funkcji.)
Heinzi,
2
@TimSchmelter Thanks. Opublikuj to jako odpowiedź, a ja ją zaakceptuję. Tak więc rzeczywisty scenariusz wyglądałby następująco:
``

Odpowiedzi:

122

Bloki przechwytywania umożliwiają już filtrowanie według typu wyjątku:

catch (SomeSpecificExceptionType e) {...}

whenKlauzula pozwala przedłużyć ten filtr do wyrażenia generycznych.

W związku z tym whenklauzula jest używana w przypadkach, gdy typ wyjątku nie jest wystarczająco wyraźny, aby określić, czy wyjątek powinien być obsługiwany w tym miejscu, czy nie.


Typowym przypadkiem użycia są typy wyjątków, które w rzeczywistości są opakowaniem dla wielu różnych rodzajów błędów.

Oto przypadek, z którego faktycznie korzystałem (w VB, który ma już tę funkcję od dłuższego czasu):

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

To samo dotyczy SqlException, które również ma ErrorCodewłaściwość. Alternatywą byłoby coś takiego:

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

który jest prawdopodobnie mniej elegancki i nieznacznie przerywa ślad stosu .

Ponadto możesz dwukrotnie wspomnieć o tym samym typie wyjątku w tym samym bloku try-catch:

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

co nie byłoby możliwe bez tego whenwarunku.

Heinzi
źródło
2
Drugie podejście też nie pozwala złapać go w inny sposób catch, prawda?
Tim Schmelter,
@TimSchmelter. Prawdziwe. Musiałbyś obsłużyć wszystkie COMExceptions w tym samym bloku.
Heinzi,
Chociaż whenumożliwia wielokrotną obsługę tego samego typu wyjątku. Warto o tym wspomnieć, ponieważ jest to zasadnicza różnica. Bez whendostaniesz błąd kompilatora.
Tim Schmelter,
1
Jeśli o mnie chodzi, pierwszą linijką odpowiedzi powinna być część następująca po „W skrócie:”.
CompuChip,
1
@ user3493289: tak jest często w przypadku brzydkiego kodu. Myślisz: „Przede wszystkim nie powinienem być w tym bałaganie, przeprojektować kod”, a także myślisz, że „mógłby być sposób na eleganckie wsparcie tego projektu, przeprojektowanie języka”. W tym przypadku nie jest to rodzaj progu jak brzydki chcesz zestawu klauzul catch być, więc coś, co sprawia, że pewne sytuacje mniej brzydki pozwala zrobić więcej w swoim progu :-)
Steve Jessop
38

Z wiki Roslyn (wyróżnienie moje):

Filtry wyjątków są lepsze niż wyłapywanie i ponowne rzucanie, ponieważ pozostawiają stos bez szwanku . Jeśli wyjątek później spowoduje zrzucenie stosu, możesz zobaczyć, skąd pochodzi, a nie tylko ostatnie miejsce, z którego został ponownie odrzucony.

Powszechną i akceptowaną formą „nadużycia” jest również stosowanie filtrów wyjątków dla skutków ubocznych; np. logowanie. Mogą obserwować „przelatujący” wyjątek bez przechwytywania jego kursu . W takich przypadkach filtr często będzie wywołaniem funkcji pomocniczej zwracającej fałsz, która wykonuje efekty uboczne:

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

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

Warto zademonstrować pierwszy punkt.

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

Jeśli uruchomimy to w WinDbg do momentu trafienia wyjątku i wydrukujemy stos używając !clrstack -i -a, zobaczymy tylko ramkę 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')

Jeśli jednak zmienimy program na używanie when:

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

Zobaczymy, że stos zawiera również Bramkę:

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

Te informacje mogą być bardzo przydatne podczas debugowania zrzutów awaryjnych.

Eli Arbel
źródło
8
To mnie zaskakuje. Czy throw;(w przeciwieństwie do throw ex;) również nie opuści stosu bez szwanku? +1 za efekt uboczny. Nie jestem pewien, czy podoba mi się to, ale dobrze jest wiedzieć o tej technice.
Heinzi,
14
Nie jest źle - to nie odnosi się do śladu stosu - odnosi się do samego stosu. Jeśli spojrzysz na stos w debugerze (WinDbg), a nawet jeśli użyłeś throw;, stos rozwija się i tracisz wartości parametrów.
Eli Arbel,
1
Może to być niezwykle przydatne podczas debugowania zrzutów.
Eli Arbel,
3
@Heinzi Zobacz moją odpowiedź w innym wątku, w którym możesz zobaczyć, że throw;zmienia to trochę ślad stosu i bardzo throw ex;go zmienia.
Jeppe Stig Nielsen
1
Użycie thrownieznacznie zakłóca ślad stosu. Numery linii są różne w przypadku używania throww przeciwieństwie do when.
Mike Zboray,
7

Gdy wyjątek jest zgłaszany, pierwszy przebieg obsługi wyjątków identyfikuje miejsce, w którym wyjątek zostanie przechwycony przed odwinięciem stosu; jeśli / kiedy zostanie zidentyfikowana lokalizacja „catch”, uruchamiane są wszystkie bloki „final” (należy zauważyć, że jeśli wyjątek ucieka przed blokiem „last”, przetwarzanie wcześniejszego wyjątku może zostać porzucone). Gdy to się stanie, kod wznowi wykonywanie w miejscu „złapania”.

Jeśli w funkcji znajduje się punkt przerwania, który jest oceniany jako część „kiedy”, ten punkt przerwania zawiesi wykonywanie przed jakimkolwiek odwinięciem stosu; W przeciwieństwie do tego, punkt przerwania w „catch” zawiesi wykonywanie dopiero po finallyuruchomieniu wszystkich programów obsługi.

Wreszcie, jeśli linie 23 i 27 foowywołania bar, a wywołanie w linii 23 zgłasza wyjątek, który zostaje przechwycony w foolinii 57 i ponownie zgłoszony, wówczas ślad stosu zasugeruje, że wyjątek wystąpił podczas wywoływania barz linii 57 [lokalizacja ponownego wyrzucenia ] , niszcząc wszelkie informacje o tym, czy wyjątek wystąpił w wywołaniu na linii 23, czy 27. Używanie w whencelu uniknięcia wyłapywania wyjątku w pierwszej kolejności pozwala uniknąć takich zakłóceń.

BTW, przydatnym wzorcem, który jest irytująco niewygodny zarówno w C #, jak i VB.NET, jest użycie wywołania funkcji w whenklauzuli w celu ustawienia zmiennej, której można użyć w finallyklauzuli, aby określić, czy funkcja została ukończona normalnie, aby obsłużyć przypadki, w których funkcja nie ma nadziei na „rozwiązanie” jakiegokolwiek wyjątku, który wystąpił, niemniej jednak musi podjąć działania na jego podstawie. Na przykład, jeśli wyjątek zostanie zgłoszony w ramach metody fabryki, która ma zwrócić obiekt, który hermetyzuje zasoby, wszelkie pozyskane zasoby będą musiały zostać zwolnione, ale bazowy wyjątek powinien przenikać do obiektu wywołującego. Najczystszym sposobem obsługi tego semantycznie (choć nie syntaktycznie) jest posiadanie rozszerzeniafinallyblokuje sprawdzenie, czy wystąpił wyjątek, a jeśli tak, zwolnij wszystkie zasoby pozyskane w imieniu obiektu, który nie będzie już zwracany. Ponieważ kod czyszczący nie ma nadziei na rozwiązanie dowolnego warunku, który spowodował wyjątek, tak naprawdę nie powinien catch, a jedynie musi wiedzieć, co się stało. Wywołanie funkcji takiej jak:

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

w whenklauzuli umożliwi funkcji fabryki wiedzieć, że coś się stało.

supercat
źródło