понедельник, 22 декабря 2014 г.

Фильтры исключений в C# 6.0

Одной из новых возможностей языка C# 6.0 являются фильтры исключений.

Общая идея довольно простая: рядом с блоком catch появляется возможность задать некоторый предикат, который будет определять, будет ли вызван блок catch или нет.

clip_image002

Данный вариант синтаксиса доступен в публичной версии VS2015, но он будет изменен в финальной версии языка C#. Вместо if будет использоваться ключевое слово when.

Фильтр исключений логически эквивалентен условию в блоке catch с последующим пробросом исключения, в случае невыполнения условия. Но в случае полноценных фильтров исключений уровня CLR, порядок исполнения будет иным.

Генерация исключения в CLR происходит следующим образом:

1. Идет поиск ближайшего блока catch, который удовлетворяет типу генерируемого исключения.

2. Исполняется предикат фильтра исключения. Если предикат возвращает true, то данный блок catch считается подходящим и начинается раскрутка стека и выполнение всех блоков finally на пути от места генерации исключения к обработчику.

3. Если фильтр исключения возвращает false, то поиск подходящего блока catch продолжается.

Это значит, что порядок исполнения генерации и обработки исключений будет таким:

ExceptionFilters

Сценарии использования

Фильтры исключения могут быть полезными в следующих случаях:

  1. Выполнить некоторое действие до раскрутки стека: например, сохранить дамп падающего процесса до вызова блоков finally, закрытия файлов или освобождения блокировок и т.п.
  2. Использовать более декларативную обработку исключений, когда одно и тоже исключение содержит еще и коды ошибок.
  3. Эмуляция блока fault CLR.

У меня ни разу не возникало необходимости в фильтрах исключения для генерации более точных дампов, но команды Roslyn и TypeScript этим пользуются.

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

clip_image005

CLR содержит особый блок обработки исключений под названием fault – аналог блока finally, но который вызывается лишь в случае исключения. Этот блок не может обработать исключение и по его завершению исключение обязательно пробрасывается дальше.

С помощью фильтров исключений можно добавить этого же поведения:

clip_image007

Первый блок catch(Exception) можно рассматривать аналогом блока fault!

В этом случае всегда будет вызываться метод LogException, после чего начнется стандартный поиск нужного блока исключения. Так, в случае генерации InvalidOperationException, оно будет вначале залогировано, а обработано блоком catch(Exception).

Пример с логированием часто приводится в качестве одного из сценариев использования фильтров исключений. Тот же Мэдс Торгесен использует его в статье “New Features in C# 6”. Использовать фильтры исключений для этих целей вполне нормально, но нужно не забывать о правильном порядке блоков catch: первым блоком должен идти catch с фильтром, всегда возвращающим false, ПОСЛЕ которого должны располагаться все остальные блоки catch.

Опасности фильтров исключений

Основная опасность фильтров исключений кроется в ее природе. Поскольку фильтры вызываются до блоков finally, то они вызываются в момент, когда блокировки еще не отпущены, файлы не закрыты, транзакции не завершены и т.д. В большинстве случаев проблем не будет, но отпилить себе ногу все же можно.

Например, генерация исключения из блока lock может легко привести к дедлогку:

clip_image009

Если CanHandle попробует захватить блокировку хитрым образом, то мы получим взаимоблокировку:

clip_image011

Мало шансов столкнуться с взаимоблокировкой в таком простом виде, но более сложные сценарии все же могут привести к проблемам.

Фильтры исключений в F#

В каждой второй статье о фильтрах исключений в C# 6.0 говорится, что эта возможность есть также в VB.NET и в F#. К VB.NET претензий нет, а вот в F# фильтров исключений нет. Точнее как, они есть, но их нетJ.

let willThrow() =
    try
        printfn "throwing..."
        failwith "Oops!"
    finally
        printfn "finally"

let check (ex: Exception) =
    printfn "check"
    true


let CheckFilters() =
    try
        willThrow()
   
with
 
        | ex
when check(ex) -> printfn "caught!"
    ()

Если запустить этот код, то вывод на экран будет таким:

throwing...
finally
check
Caught!

Фильтры исключений в F# не используют фильтры исключений CLR – это обычное выражение сопоставления с образцом!

5 комментариев:

  1. Отлично, спасибо. Но, кажется, не хватает хотя бы ссылки на то, что есть "раскрутка стека".

    ОтветитьУдалить
    Ответы
    1. Ссылка-то есть, но видно слишком уж мимоходом, что можно пропустить:

      > 2. Исполняется предикат фильтра исключения. Если предикат возвращает true, то данный блок catch считается подходящим и начинается раскрутка стека и выполнение всех блоков finally на пути от места генерации исключения к обработчику.

      Удалить
  2. Статья понравилась, да и читал сразу как появилась, но забыл отписать пару маленьких замечаний/вопросов:
    - разве блок finally выполняется до блока catch??
    - по примеру с дедлоком - а разве ConfigureAwait(false) не поможет?

    ОтветитьУдалить
    Ответы
    1. finally выполняется до блока catch, но фильтр исключений выполняются ДО выполнения блока finally.
      И именно поэтому ConfigureAwait(false) не поможет: фильтр вызывается до вызова finally, а значит блокировки, захваченные в блоке try все еще не освобождены.

      Удалить
    2. Извини, но наверное я тебя не так понял - как это finally выполняется раньше catch? Это новое поведение?

      Удалить