Сообщений 3    Оценка 20        Оценить  
Система Orphus

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

Автор: Тепляков Сергей Владимирович
Опубликовано: 18.11.2015
Исправлено: 10.12.2016
Версия текста: 1.1

Сценарии использования
Опасности фильтров исключений
Фильтры исключений в F#

Одной из новых возможностей языка 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 происходит следующим образом:

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

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

      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");
  }
}

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

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

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

У меня ни разу не возникало необходимости в фильтрах исключения для генерации более точных дампов, но команды 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;
}

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

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

В каждой второй статье о фильтрах исключений в 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        Оценить