Одной из новых возможностей языка C# 6.0 являются фильтры исключений.
Общая идея довольно простая: рядом с блоком catch появляется возможность задать некоторый предикат, который будет определять, будет ли вызван блок catch или нет.
Данный вариант синтаксиса доступен в публичной версии VS2015, но он будет изменен в финальной версии языка C#. Вместо if будет использоваться ключевое слово when.
Фильтр исключений логически эквивалентен условию в блоке catch с последующим пробросом исключения, в случае невыполнения условия. Но в случае полноценных фильтров исключений уровня CLR, порядок исполнения будет иным.
Генерация исключения в CLR происходит следующим образом:
1. Идет поиск ближайшего блока catch, который удовлетворяет типу генерируемого исключения.
2. Исполняется предикат фильтра исключения. Если предикат возвращает true, то данный блок catch считается подходящим и начинается раскрутка стека и выполнение всех блоков finally на пути от места генерации исключения к обработчику.
3. Если фильтр исключения возвращает false, то поиск подходящего блока catch продолжается.
Это значит, что порядок исполнения генерации и обработки исключений будет таким:
Сценарии использования
Фильтры исключения могут быть полезными в следующих случаях:
- Выполнить некоторое действие до раскрутки стека: например, сохранить дамп падающего процесса до вызова блоков finally, закрытия файлов или освобождения блокировок и т.п.
- Использовать более декларативную обработку исключений, когда одно и тоже исключение содержит еще и коды ошибок.
- Эмуляция блока fault CLR.
У меня ни разу не возникало необходимости в фильтрах исключения для генерации более точных дампов, но команды Roslyn и TypeScript этим пользуются.
Второй сценарий использования связан с тем, что коды ошибок иногда проникают в исключения. SqlException содержит код ошибки, что может приводить к их императивному анализу, вместо использования разных блоков catch. Фильтры исключений здесь могут сильно помочь:
CLR содержит особый блок обработки исключений под названием fault – аналог блока finally, но который вызывается лишь в случае исключения. Этот блок не может обработать исключение и по его завершению исключение обязательно пробрасывается дальше.
С помощью фильтров исключений можно добавить этого же поведения:
Первый блок catch(Exception) можно рассматривать аналогом блока fault!
В этом случае всегда будет вызываться метод LogException, после чего начнется стандартный поиск нужного блока исключения. Так, в случае генерации InvalidOperationException, оно будет вначале залогировано, а обработано блоком catch(Exception).
Пример с логированием часто приводится в качестве одного из сценариев использования фильтров исключений. Тот же Мэдс Торгесен использует его в статье “New Features in C# 6”. Использовать фильтры исключений для этих целей вполне нормально, но нужно не забывать о правильном порядке блоков catch: первым блоком должен идти catch с фильтром, всегда возвращающим false, ПОСЛЕ которого должны располагаться все остальные блоки catch.
Опасности фильтров исключений
Основная опасность фильтров исключений кроется в ее природе. Поскольку фильтры вызываются до блоков finally, то они вызываются в момент, когда блокировки еще не отпущены, файлы не закрыты, транзакции не завершены и т.д. В большинстве случаев проблем не будет, но отпилить себе ногу все же можно.
Например, генерация исключения из блока lock может легко привести к дедлогку:
Если CanHandle попробует захватить блокировку хитрым образом, то мы получим взаимоблокировку:
Мало шансов столкнуться с взаимоблокировкой в таком простом виде, но более сложные сценарии все же могут привести к проблемам.
Фильтры исключений в 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 – это обычное выражение сопоставления с образцом!
Отлично, спасибо. Но, кажется, не хватает хотя бы ссылки на то, что есть "раскрутка стека".
ОтветитьУдалитьСсылка-то есть, но видно слишком уж мимоходом, что можно пропустить:
Удалить> 2. Исполняется предикат фильтра исключения. Если предикат возвращает true, то данный блок catch считается подходящим и начинается раскрутка стека и выполнение всех блоков finally на пути от места генерации исключения к обработчику.
Статья понравилась, да и читал сразу как появилась, но забыл отписать пару маленьких замечаний/вопросов:
ОтветитьУдалить- разве блок finally выполняется до блока catch??
- по примеру с дедлоком - а разве ConfigureAwait(false) не поможет?
finally выполняется до блока catch, но фильтр исключений выполняются ДО выполнения блока finally.
УдалитьИ именно поэтому ConfigureAwait(false) не поможет: фильтр вызывается до вызова finally, а значит блокировки, захваченные в блоке try все еще не освобождены.
Извини, но наверное я тебя не так понял - как это finally выполняется раньше catch? Это новое поведение?
Удалить