Виловлювання винятків за допомогою “catch, when”


94

Я натрапив на цю нову функцію в 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;
}

Чи сприяє функція "catch, when" обробку винятків швидше, оскільки обробник пропускається як такий, і розмотування стека може відбутися набагато раніше, ніж у порівнянні з обробкою конкретних випадків використання в обробнику? Чи існують якісь конкретні випадки використання, які більше відповідають цій функції, які люди можуть потім застосувати як належну практику?


8
Це корисно, якщо whenпотрібно отримати доступ до самого винятку
Тім Шмельтер

1
Але це те, що ми можемо зробити як у самому блоці обробника, так і правильно. Чи є якісь переваги, крім "трохи більш організованого коду"?
MS Srikkanth,

3
Але тоді ви вже обробили виняток, якого не хочете. Що робити, якщо ви хочете зловити це десь ще в цьому try..catch...catch..catch..finally?
Тім Шмельтер

4
@ user3493289: Після цього аргументу нам також не потрібні автоматичні перевірки типу в обробниках винятків: ми можемо дозволити catch (Exception ex), перевірити тип та throwінше. Дещо більш організований код (він же уникає шуму коду) - саме тому існує ця функція. (Це насправді справедливо для багатьох функцій.)
Хайнзі,

2
@TimSchmelter Дякую. Опублікуйте це як відповідь, і я прийму. Тож фактичним сценарієм буде тоді "якщо умова обробки залежить від винятку", то використовуйте цю функцію /
MS Srikkanth

Відповіді:


118

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

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
Другий підхід також не дозволяє зловити його в іншому catch, правда?
Тім Шмельтер

@TimSchmelter. Правда. Вам доведеться обробляти всі винятки COME в одному блоці.
Heinzi

Тоді як whenдозволяє обробляти один і той же тип винятків кілька разів. Ви також повинні це згадати, оскільки це суттєва різниця. Без цього whenви отримаєте помилку компілятора.
Тім Шмельтер

1
Що стосується мене, то перший рядок відповіді повинен бути частиною, що слідує "У двох словах:".
CompuChip

1
@ user3493289: це часто трапляється з потворним кодом. Ви думаєте: "Я не повинен бути в цій халепі, перш за все, перепроектувати код", і ти також думаєш "може бути спосіб підтримати цей дизайн елегантно, переробити мову". У цьому випадку існує своєрідний поріг того, наскільки потворним ви хочете, щоб ваш набір пропозицій catch був, тож щось, що робить певні ситуації менш потворними, дозволяє вам більше робити в межах вашого порогу :-)
Стів Джессоп,

37

З вікі Росліна (наголос мій):

Фільтри винятків є кращими для лову та перекидання, оскільки вони залишають стек неушкодженим . Якщо виняток пізніше спричиняє скидання стека, ви можете побачити, звідки він взявся, а не лише останнє місце, яке було відновлено.

Також поширеною та прийнятою формою “зловживання” є використання фільтрів винятків для побічних ефектів; наприклад, лісозаготівля. Вони можуть перевірити виняток, що "пролітає", не перехоплюючи його шляху . У цих випадках фільтр часто є викликом допоміжної функції, що повертає помилково, яка виконує побічні ефекти:

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;, стек розкручується, і ви втрачаєте значення параметрів.
Eli Arbel

1
Це може бути надзвичайно корисним при налагодженні дампів.
Eli Arbel

3
@Heinzi Дивіться мою відповідь в іншій темі, де ви можете побачити, що throw;трохи throw ex;змінює трасування стека і сильно його змінює.
Jeppe Stig Nielsen

1
Використання throwдещо порушує трасування стека. Номери рядків відрізняються при використанні throwна відміну від when.
Mike Zboray

7

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

Якщо у функції є точка зупинку, яка обчислюється як частина "коли", ця точка зупинки призупинить виконання до того, як відбудеться будь-яке розкручування стека; навпаки, точка зупинки при "лові" призупинить виконання лише після finallyзапуску всіх обробників.

Нарешті, якщо рядки 23 і 27 fooвиклику bar, і виклик на лінії 23 видає виняток, який потрапляє всередину fooі відновлюється на рядку 57, то трасування стека буде припускати, що виняток стався під час виклику barз рядка 57 [місце повторного виклику] , знищуючи будь-яку інформацію про те, чи сталося виняток у дзвінку лінії-23 або лінії-27. Використання , whenщоб уникнути ловити виключення в першу чергу дозволяє уникнути такого порушення.

До речі, корисний шаблон, який досадно незручний як у C #, так і у VB.NET, полягає у використанні виклику функції в whenреченні для встановлення змінної, яка може використовуватися в finallyреченні для визначення того, чи функція завершена нормально, для обробки випадків, коли функція не має надії "вирішити" будь-який виняток, що трапляється, але, тим не менш, повинен вжити заходів, заснованих на ньому. Наприклад, якщо виключення викидається в рамках заводського методу, який повинен повертати об'єкт, який інкапсулює ресурси, будь-які отримані ресурси потрібно буде звільнити, але основний виняток повинен передаватися до абонента. Найчистіший спосіб обробити це семантично (хоча і не синтаксично) - це матиfinallyблок перевірити, чи не сталося виняток, і, якщо так, звільнити всі ресурси, придбані від імені об'єкта, який більше не збирається повертати. Оскільки код очищення не має надії вирішити будь-яку умову, що спричинила виняток, він цього не повинен catch, а просто повинен знати, що сталося. Виклик такої функції, як:

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

у whenпункті дасть змогу заводській функції дізнатися, що щось сталося.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.