Улавливание исключений с помощью "catch, when"

Я столкнулся с этой новой функцией в С#, которая позволяет обработчику catch выполнять, когда выполняется конкретное условие.

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

Я пытаюсь понять, когда это может когда-либо быть полезным.

Один сценарий может быть примерно таким:

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

но это снова то, что я могу сделать в рамках одного и того же обработчика и делегировать разные методы в зависимости от типа драйвера. Означает ли это, что код легче понять? Возможно, нет.

Другой сценарий, о котором я могу думать, это что-то вроде:

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

Снова это то, что я могу сделать, например:

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

Использует ли функция "catch, when" сделать обработку исключений быстрее, потому что обработчик пропущен как таковой, и раскрутка стека может произойти намного раньше, чем при обработке конкретных случаев использования в обработчике? Существуют ли конкретные варианты использования, которые лучше подходят для этой функции, которую люди могут принять в качестве хорошей практики?

Ответ 1

Блокировка блоков уже позволяет вам фильтровать тип исключения:

catch (SomeSpecificExceptionType e) {...}

Предложение when позволяет распространить этот фильтр на общие выражения.

Таким образом, вы используете предложение when для случаев, когда тип исключения не достаточно различен, чтобы определить, нужно ли здесь обрабатывать исключение.


Общим примером использования являются типы исключений, которые на самом деле являются оберткой для нескольких разных ошибок.

Здесь случай, который я фактически использовал (в VB, который уже имеет эту функцию довольно долгое время):

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

То же самое для SqlException, которое также имеет свойство ErrorCode. Альтернативой было бы что-то вроде этого:

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

который, пожалуй, менее изящный, и слегка разбивает трассировку стека.

Кроме того, вы можете упомянуть тот же тип исключения дважды в одном блоке try-catch:

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

что было бы невозможно без условия when.

Ответ 2

От Roslyn wiki (внимание мое):

Фильтры исключений предпочтительнее ловли и реинтродирования, потому что они оставляют стек невредимым. Если последующее исключение вызывает стек вы можете увидеть, откуда он изначально появился, а не просто последнее место было заново заново.

Это также распространенная и принятая форма "злоупотребления" для использования исключения фильтры для побочных эффектов; например Ведение журнала. Они могут проверять исключение "летать", не перехватывая его курс. В этих случаях фильтр часто является вызовом функции возврата с ложным возвратом, которая выполняет побочные эффекты:

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

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

Первый момент стоит продемонстрировать.

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

Если мы запустим это в WinDbg до тех пор, пока не будет удалено исключение, и распечатайте стек с помощью !clrstack -i -a, мы увидим только фрейм 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')

Однако, если мы изменим программу на использование when:

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

Мы увидим, что стек также содержит фрейм 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')

Эта информация может быть очень полезна при отладке аварийных дампов.

Ответ 3

Когда генерируется исключение, первый проход обработки исключений определяет, где исключение будет поймано, прежде чем разматывать стек; если/когда идентифицируется местоположение "catch" , все "окончательные" блоки запускаются (обратите внимание, что если исключение выходит из "окончательного" блока, обработка более раннего исключения может быть отменена). Как только это произойдет, код возобновит выполнение в "catch" .

Если в функции, которая оценивается как часть "когда", есть точка останова, эта точка останова приостанавливает выполнение до того, как произойдет разворачивание стека; напротив, точка останова при "catch" будет только приостанавливать выполнение после выполнения всех обработчиков finally.

Наконец, если строки 23 и 27 из foo вызывают bar, а вызов в строке 23 вызывает исключение, которое попадает в foo и возвращается в строку 57, тогда трассировка стека предполагает, что исключение произошел при вызове bar из строки 57 [location of rethrow], уничтожив любую информацию о том, произошло ли исключение в вызове line-23 или line-27. Использование when во избежание исключения исключения в первую очередь позволяет избежать такого нарушения.

BTW, полезный шаблон, который раздражает неловко как в С#, так и в VB.NET, заключается в использовании вызова функции в предложении when для установки переменной, которая может использоваться в предложении finally, чтобы определить, является ли функция как правило, обрабатывать случаи, когда функция не надеется "разрешить" любое исключение, которое имеет место, но тем не менее должно принять меры на его основе. Например, если исключение выбрано внутри метода factory, который должен возвращать объект, который инкапсулирует ресурсы, все ресурсы, которые были получены, должны быть освобождены, но основное исключение должно просачиваться до вызывающего. Самый чистый способ обработки семантически (хотя и не синтаксически) состоит в том, чтобы блок finally проверял, произошло ли исключение, и если да, освободите все ресурсы, полученные от имени объекта, который больше не будет возвращен. Так как код очистки не имеет надежды на то, чтобы разрешить любое условие, вызвавшее исключение, оно действительно не должно catch, а просто нужно знать, что произошло. Вызов функции типа:

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

в рамках предложения when позволит функции factory знать что-то случилось.