Сообщений 3 Оценка 20 Оценить |
Одной из новых возможностей языка C# 6.0 являются фильтры исключений.
Общая идея довольно простая: рядом с блоком catch появляется возможность задать некоторый предикат, который будет определять, будет ли вызван блок catch.
private bool _shouldHandle = false; publicvoid CrazySample() { try { SomeMethod(); } catch(Exception e) if (e.message == "42" && _shouldHandle) { // Handle exception } } |
Данный вариант синтаксиса доступен в публичной версии VS2015, но он будет изменен в финальной версии языка C#. Вместо if будет использоваться ключевое слово when.
Фильтр исключений логически эквивалентен условию в блоке catch с последующим пробросом исключения в случае невыполнения условия. Но, в случае полноценных фильтров исключений уровня CLR, порядок исполнения будет иным.
Генерирование исключения в CLR происходит следующим образом:
Это значит, что порядок исполнения генерации и обработки исключений будет таким:
public void Run() { try { MethodThatThrows(); } catch(UnvalidOperationException) //if (true) { } catch(ArgumentException e) if (CanHandle(e.message == "Msg", "AE")) { Console.WriteLine("Caught AE"); } catch(Exception e) if (CanHandle(true, "E")) { Console.WriteLine("Caught Exception"); } } privatebool CanHandle(bool canHandle, string msg) { Console.WriteLine("Canhandle {0}? {1}", msg, canHandle); return canHandle; } privatevoid MethodThatThrows(); { try { trow new ArgumentException("Ooops!") } catch(Exception) { Console.WriteLine("MethodThatThrows.catch"); throw; } finally { Console.WriteLine("MethodThatThrows.finally"); } } |
Фильтры исключения могут быть полезными в следующих случаях:
У меня ни разу не возникало необходимости в фильтрах исключения для генерации более точных дампов, но команды Roslyn и TypeScript этим пользуются.
Второй сценарий использования связан с тем, что коды ошибок иногда проникают в исключения. SqlException содержит код ошибки, что может приводить к их императивному анализу вместо использования разных блоков catch. Фильтры исключений здесь могут сильно помочь:
private void ExecuteSql() { try { RunSqlServerCommand(); } catch (SqlException sex) if (sex.Number == 547) // FK violation { HandleForeignKeyViolation(); } catch (SqlException sex) if (sex.Number == 42) // PK violation { HandlePrimaryKeyViolation(); } } |
CLR содержит особый блок обработки исключений под названием fault – аналог блока finally, но который вызывается лишь в случае исключения. Этот блок не может обработать исключение и по его завершению исключение обязательно пробрасывается дальше.
С помощью фильтров исключений можно добавить этого же поведения:
public void Sample() { try { Throw(); } // Should be first catch block!catch (Exception e) if (LogException(e)) { } catch(ArgumentException) {} catch(Exception) { Console.WriteLine("Caught Exception!"); } } privatebool LogException(Exception e) { Console.WriteLine("Exception: " + e); returnfalse; } |
Первый блок catch(Exception) можно рассматривать аналогом блока fault!
В этом случае всегда будет вызываться метод LogException, после чего начнется стандартный поиск нужного блока исключения. Так, в случае генерации InvalidOperationException, оно будет вначале залогировано, а обработано блоком catch(Exception).
Пример с логированием часто приводится в качестве одного из сценариев использования фильтров исключений. Тот же Мэдс Торгесен использует его в статье “New Features in C# 6”. Использовать фильтры исключений для этих целей вполне нормально, но нужно не забывать о правильном порядке блоков catch: первым блоком должен идти catch с фильтром, всегда возвращающим false, ПОСЛЕ которого должны располагаться все остальные блоки catch.
Основная опасность фильтров исключений кроется в их природе. Поскольку фильтры вызываются до блоков finally, то они вызываются в момент, когда блокировки еще не отпущены, файлы не закрыты, транзакции не завершены и т.д. В большинстве случаев проблем не будет, но отпилить себе ногу все же можно.
Например, генерация исключения из блока lock может легко привести к дедлоку:
private readonly object _syncRoot = newobject(); privatevoid CrazyMethod() { lock(_syncRoot) { Console.WriteLine("Throwing from lock!"); thrownew Exception("Ooops!") } } publicvoid Deadlock() { try { CrazyMethod(); } catch(Exception e) if(CanHandle(e)) { Console.WriteLine("Handled!"); } } |
Если CanHandle попробует захватить блокировку хитрым образом, то мы получим взаимоблокировку:
private bool CanHandle(Exception e) { Task.Run(() => { Console.WriteLine("Trying to handle an exception!"); lock(_syncRoot) {} }).Wait(); returntrue; } |
Мало шансов столкнуться с взаимоблокировкой в таком простом виде, но более сложные сценарии все же могут привести к проблемам.
В каждой второй статье о фильтрах исключений в C# 6.0 говорится, что эта возможность есть также в VB.NET и в F#. К VB.NET претензий нет, а вот в F# фильтров исключений нет. Точнее как, они есть, но их нет. :)
let willThrow() = try printfn "throwing..." failwith "Oops!" finally printfn "finally"let check (ex: Exception) = printfn "check" truelet CheckFilters() = try willThrow() with | ex when check(ex) -> printfn "caught!" () |
Если запустить этот код, то вывод на экран будет таким:
throwing... finally check Caught! |
Фильтры исключений в F# не используют фильтры исключений CLR – это обычное выражение сопоставления с образцом!
Сообщений 3 Оценка 20 Оценить |