Что такое фильтры исключений?
Фильтры исключений (Exception Filters) — новая фича C# 6, которая позволяет устанавливать специфические условия для блока catch
. Этот блок будет исполнятся только в случае, если указанные условия выполнены. Проиллюстрируем синтаксис небольшим фрагментом кода:
public void Main()
{
try
{
throw new Exception("E2");
}
catch(Exception ex) when(ex.Message == "E1")
{
Console.WriteLine("caught E1");
}
catch(Exception ex) when(ex.Message == "E2")
{
Console.WriteLine("caught E2");
}
}
Это правда новая фича?
Для C# — да. Впрочем, поддержка фильтров исключений уже давно присутствует в IL и VB.NET. Даже язык F# поддерживает эти фильтры с помощью механизма под названием exception pattern matching.
А можем ли мы получить такую функциональность с помощью обычных условных операторов?
Логически — да, но есть фундаментальное отличие. Если условие находится внутри catch
-блока, то сначала исключение будет поймано, а затем произойдёт проверка условия. Фильтры исключений проверяют условие до поимки исключения. Если условие не выполняется, то catch
-блок будет пропущен, .NET перейдёт к рассмотрению следующего catch
-блока.
Так в чём же разница?
Когда вы ловите исключение, то имеете размотанный стек, т.е. теряете важную информацию об исключении. Существует ошибочное мнение, что если мы выполним throw
внутри catch
-блока вместо throw ex
, то сохраним стек. Дело в том, что люди задумываются только о свойстве исключения StackTrace
, но не о самом стеке CLR. Давайте рассмотрим пример. Если вы будете пробовать запустить его самостоятельно, то убедитесь, что галочка «Break on Exception» для поимки исключений выключена в настройках Visual Studio (Debug->Exceptions->Uncheck all).
Рассмотрим общий сценарий, в котором мы ловим исключение, логируем его и больше ничего не делаем. Следующее изображение показывает где остановится отладчик в момент бросания исключения. Обратите внимание на строчку остановки отладчика и на окно Locals:
Теперь давайте немного перепишем пример с использованием фильтров исключений. Сделаем так, чтобы метод Log
всегда возвращал false
и выполним логирование с помощью фильтров исключений вместо того, чтобы помещать его внутрь catch
-блока. Снова обратите внимания на строчку остановки отладчика и окно отладчика (Примечание: пост и пример были обновлены, но картинка осталась прежней; вместо catch (if(Log()))
следует читать catch when (Log())
):
От переводчика: оригинальная картинка устарела, т. к. в недавнем прошлом синтаксис фильтров исключений немного поменялся: раньше они описывались с помощью ключевого слова if
, а теперь его заменили на новое ключевое слово when
. Мотивация хорошо иллюстрируется следующей картинкой:
Помимо информации о том, где точно произошло исключение, вы также можете видеть в окне Locals второго примера локальную переменную localVariable
, которая недоступна в первом примере, т. к. внутри catch
-блока она не находится в стеке. Это соответствует тому, что бы можем видеть в crash-дампах.
Кроме того, если вы вошли в какой-то catch
-блок, то в остальные уже не войдёте. Если же вы проанализировали условие и решили не выбрасывать его снова, то вы уже не сможете попасть в другие catch
-блоки. В случае фильтров исключений невыполненное условие не помешает нам проверить остальные catch
-условия, чтобы попытаться зайти в другой блок.
Ожидаемое поведение
Таким образом, мы можем указать условие в фильтре исключений; catch
-блок будет исполняться только в том случае, если условие выполнилось. И мы можем использовать bool
-функцию в качестве условия. Но что произойдёт, если само условие выбросит исключение? Ожидаемым поведением является следующее: исключение игнорируется, условия считается ложным. Рассмотрим следующий код:
class Program
{
public static void Main()
{
TestExceptionFilters();
}
public static void TestExceptionFilters()
{
try
{
throw new Exception("Original Exception");
}
catch (Exception ex) when (MyCondition())
{
Console.WriteLine(ex);
}
}
public static bool MyCondition()
{
throw new Exception("Condition Exception");
}
}
В момент выбрасывания исключения "Original Exception"
перед заходом в catch
-блок будет проверено условие MyCondition
. Но это условие само выбрасывает исключение, которое должно быть проигнорировано, а условие должно считаться ложным. Таким образом, мы получим необработанное исключение:
System.Exception: Original Exception
Неожиданное поведение
Настало время странного примера. Изменим приведённый выше код так, что вместо прямого вызова метода TestExceptionFilters()
, этот метод будет вызываться через рефлексию. Ожидаемое поведение остаётся прежним, хоть мы и вызываем функцию иначе.
class Program
{
public static void Main()
{
var targetMethod = typeof(Program).GetMethod("TestExceptionFilters");
targetMethod.Invoke(null, new object[0]);
}
public static void TestExceptionFilters()
{
try
{
throw new Exception("Original exception");
}
catch (Exception ex) when (MyCondition())
{
Console.WriteLine(ex);
}
}
public static bool MyCondition()
{
throw new Exception("Condition Exception");
}
}
Давайте запустим этот код. Как и ожидалось, мы получим необработанное исключение, но вот только тип исключения будет другим:
System.Exception: Condition Exception
Таким образом, тип исключения зависит от способа, которым мы взывали функцию. Про этот баг заведён issue на GitHub (Exception filters have different behavior when invoked via reflection and the filter throws an exception). На момент написания перевода баг всё ещё присутствует в CoreCLR. Будем надеяться, что кто-нибудь вскоре его поправит.
От переводчика: данный пост является составным переводом сразу двух постов с сайта www.volatileread.com: Unpredictable Behavior With C# 6 Exception Filters и C# 6 Exception Filters and How they are much more than Syntactic Sugar
Автор: DreamWalker