Отлов исключений с помощью «поймать, когда»

95

Я столкнулся с этой новой функцией C #, которая позволяет обработчику 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;
}

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

MS Srikkanth
источник
9
Это полезно, если whenнеобходимо получить доступ к самому исключению
Тим Шмелтер
1
Но это то, что мы можем сделать и в самом блоке обработчика, правильно. Есть ли какие-то преимущества помимо «немного более организованного кода»?
MS Srikkanth
3
Но тогда вы уже обработали исключение, которое вам не нужно. Что, если вы хотите поймать это где-нибудь еще в этом try..catch...catch..catch..finally?
Тим Шмелтер
4
@ user3493289: Следуя этому аргументу, нам также не нужны автоматические проверки типов в обработчиках исключений: мы можем только разрешить catch (Exception ex), проверить тип и throwиначе. Чуть более организованный код (он же избегание шума кода) - вот почему существует эта функция. (На самом деле это верно для многих функций.)
Heinzi
2
@TimSchmelter Спасибо. Отправьте это как ответ, и я приму его. Таким образом, реальным сценарием будет «если условие обработки зависит от исключения», тогда используйте эту функцию /
MS Srikkanth

Ответы:

120

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

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условия.

Heinzi
источник
2
Второй подход тоже не позволяет поймать по другому catch, не так ли?
Тим Шмельтер
@TimSchmelter. Правда. Вам придется обрабатывать все COMExceptions в одном блоке.
Heinzi
В то время как whenпозволяет обрабатывать один и тот же тип исключения несколько раз. Вы должны упомянуть и об этом, поскольку это принципиальная разница. Без этого whenвы получите ошибку компилятора.
Tim Schmelter
1
Насколько я понимаю, часть, следующая за «В двух словах:», должна быть первой строкой ответа.
CompuChip
1
@ user3493289: это часто бывает с уродливым кодом. Вы думаете: «Во-первых, я не должен быть в этой неразберихе, переделайте код», и вы также думаете, что «может быть способ элегантно поддержать этот дизайн, переделать язык». В этом случае есть своего рода порог того, насколько уродливым вы хотите, чтобы ваш набор предложений catch был, поэтому что-то, что делает определенные ситуации менее уродливыми, позволяет вам делать больше в пределах вашего порога :-)
Стив Джессоп
38

Из вики Roslyn (выделено мной):

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

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

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

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

Эли Арбель
источник
7
Это меня удивляет. Не оставит ли стек целым throw;(в противоположность throw ex;)? +1 за побочный эффект. Не уверен, что одобряю это, но хорошо знать об этой технике.
Heinzi
13
Это не так - это не относится к трассировке стека - это относится к самому стеку. Если вы посмотрите на стек в отладчике (WinDbg), и даже если вы использовали throw;, стек раскручивается, и вы теряете значения параметров.
Эли Арбель
1
Это может быть чрезвычайно полезно при отладке дампов.
Эли Арбель
3
@Heinzi См. Мой ответ в другом потоке, где вы можете видеть, что throw;это немного throw ex;меняет трассировку стека и сильно меняет.
Jeppe Stig Nielsen
1
Использование throwнемного нарушает трассировку стека. Номера строк при использовании отличаются throwот when.
Mike Zboray
7

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

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

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

Кстати, полезный шаблон, который досадно неудобен как в C #, так и в VB.NET, заключается в использовании вызова функции в whenпредложении для установки переменной, которая может использоваться в finallyпредложении, чтобы определить, нормально ли завершилась функция, для обработки случаев, когда функция не имеет надежды «разрешить» возникшее исключение, но, тем не менее, должен принять меры на его основе. Например, если в фабричном методе генерируется исключение, которое должно возвращать объект, инкапсулирующий ресурсы, любые ресурсы, которые были получены, необходимо будет освободить, но базовое исключение должно распространяться до вызывающего. Самый чистый способ справиться с этим семантически (хотя и не синтаксически) - это иметьfinallyblock проверяет, произошло ли исключение, и, если да, освобождает все ресурсы, полученные от имени объекта, который больше не будет возвращаться. Поскольку код очистки не имеет никакой надежды на разрешение любого условия, вызвавшего исключение, на самом деле он не должен catchэтого делать, а просто должен знать, что произошло. Вызов такой функции, как:

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

внутри whenпредложения позволит фабричной функции узнать, что что-то произошло.

суперкар
источник