Эта статья посвящена фильтрам исключений в C# 6. Есть уже
замечательная статья, написанная Сергеем Тепляковым, которая называется "Фильтры исключений в C# 6.0". Мы же постараемся рассмотреть фильтры
исключений со стороны IL-кода, так как эта тема вызывает некоторые недопонимания у многих разработчиков, плюс она актуальна в связи с выходом в свет новой версии Visual Studio 2015 CTP 6, которую можно скачать по ссылке, и в которой уже добавлены новые
изменения языка C# (в частности, замена для фильтра исключений оператора if на оператор when).
Для начала приведу два куска кода, отличающиеся друг от друга, а затем вернусь к причинам написания этой статьи.
Для начала приведу два куска кода, отличающиеся друг от друга, а затем вернусь к причинам написания этой статьи.
Ниже приведен код, написанный на C# 6 в последней редакции Visual Studio 2015.
class Program
{
static void
Main(string[] args)
{
try
{
TestMessage(null);
}
catch
(InvalidOperationException ex) when (ex.Message == "message test can't be null")
{
WriteLine("Catch filtered exception");
}
catch
(Exception ex)
{
WriteLine("Catch other exceptions");
}
ReadLine();
}
public static void TestMessage(string message)
{
if
(string.IsNullOrEmpty(message))
throw new InvalidOperationException("message test can't be null");
Console.WriteLine(message);
}
}
И код, который написан в C# 5:
class Program
{
static void
Main(string[] args)
{
try
{
TestMessage(null);
}
catch
(InvalidOperationException ex)
{
if(ex.Message
== "message test
can't be null")
Console.WriteLine("Catch
filtered exception");
else
{
throw;
}
}
catch
(Exception ex)
{
Console.WriteLine("Catch
other exceptions");
}
Console.ReadLine();
}
public static void TestMessage(string message)
{
if
(string.IsNullOrEmpty(message))
throw new InvalidOperationException("message test can't be null");
Console.WriteLine(message);
}
}
Итак, начнем с этих двух примеров. Дело в том, что в более
ранних версиях Visual Studio 2015 и языка C# (он постоянно изменяется, и пока нет окончательной версии) вместо оператора when, использовался
оператор if. Возможно, именно этот факт приводил к тому, что
некоторые разработчики считали два этих примера эквивалентными с точки зрения
того, что для них фильтры были своего рода синтаксическим сахаром, который можно
переписать в код, который вы можете увидеть для C# 5. И это небеспочвенно.
Давайте начнем, пожалуй, с самого основного отличия этих двух кусков
кода. Фильтры в C# 6 позволяют
задать рядом с блоком catch условие, при котором блок catch будет вызван или
же нет. Если условие возвращает true, то это значит, что
найденный блок отвечает требованиям, и начинается раскрутка стека с вызовом всех
блоков finally от места генерации
исключения к обработчику.
Давайте посмотрим, что же происходит в этом коде на уровне IL-кода:
// Methods
.method private hidebysig static
void Main (
string[] args
) cil managed
{
// Method begins at RVA 0x2050
// Code size 74 (0x4a)
.maxstack 2
.entrypoint
.locals init (
[0] class [mscorlib]System.InvalidOperationException ex,
[1] class [mscorlib]System.Exception ex,
[2] bool CS$4$0000
)
IL_0000: nop
.try
{
IL_0001: nop
IL_0002: ldnull
IL_0003: call void ExceptionFilters.Program::TestMessage(string)
IL_0008: nop
IL_0009: nop
IL_000a: leave.s IL_0042
} // end .try
catch [mscorlib]System.InvalidOperationException
{
IL_000c: stloc.0
IL_000d: nop
IL_000e: ldloc.0
IL_000f: callvirt instance string [mscorlib]System.Exception::get_Message()
IL_0014: ldstr "message test can't be null"
IL_0019: call bool [mscorlib]System.String::op_Equality(string, string)
IL_001e: ldc.i4.0
IL_001f: ceq
IL_0021: stloc.2
IL_0022: ldloc.2
IL_0023: brtrue.s IL_0030
IL_0025: ldstr "Catch filtered exception"
IL_002a: call void [mscorlib]System.Console::WriteLine(string)
IL_002f: nop
IL_0030: rethrow
} // end handler
catch [mscorlib]System.Exception
{
IL_0032: stloc.1
IL_0033: nop
IL_0034: ldstr "Catch other exceptions"
IL_0039: call void [mscorlib]System.Console::WriteLine(string)
IL_003e: nop
IL_003f: nop
IL_0040: leave.s IL_0042
} // end handler
IL_0042: nop
IL_0043: call string [mscorlib]System.Console::ReadLine()
IL_0048: pop
IL_0049: ret
} // end of method Program::Main
.method private hidebysig static
void Main (
string[] args
) cil managed
{
// Method begins at RVA 0x2050
// Code size 74 (0x4a)
.maxstack 2
.entrypoint
.locals init (
[0] class [mscorlib]System.InvalidOperationException ex,
[1] class [mscorlib]System.Exception ex,
[2] bool CS$4$0000
)
IL_0000: nop
.try
{
IL_0001: nop
IL_0002: ldnull
IL_0003: call void ExceptionFilters.Program::TestMessage(string)
IL_0008: nop
IL_0009: nop
IL_000a: leave.s IL_0042
} // end .try
catch [mscorlib]System.InvalidOperationException
{
IL_000c: stloc.0
IL_000d: nop
IL_000e: ldloc.0
IL_000f: callvirt instance string [mscorlib]System.Exception::get_Message()
IL_0014: ldstr "message test can't be null"
IL_0019: call bool [mscorlib]System.String::op_Equality(string, string)
IL_001e: ldc.i4.0
IL_001f: ceq
IL_0021: stloc.2
IL_0022: ldloc.2
IL_0023: brtrue.s IL_0030
IL_0025: ldstr "Catch filtered exception"
IL_002a: call void [mscorlib]System.Console::WriteLine(string)
IL_002f: nop
IL_0030: rethrow
} // end handler
catch [mscorlib]System.Exception
{
IL_0032: stloc.1
IL_0033: nop
IL_0034: ldstr "Catch other exceptions"
IL_0039: call void [mscorlib]System.Console::WriteLine(string)
IL_003e: nop
IL_003f: nop
IL_0040: leave.s IL_0042
} // end handler
IL_0042: nop
IL_0043: call string [mscorlib]System.Console::ReadLine()
IL_0048: pop
IL_0049: ret
} // end of method Program::Main
Как видим из примера, даже не разбираясь особо в IL-коде, можно заметить, что у нас следуют блоки try-> catch, и лишь затем в блоке catch идет проверка.
А теперь тот же пример, написанный на C# 6 и который мы разберем также с помощью ILSpy.
// Methods
.method private hidebysig static
void Main (
string[] args
) cil managed
{
// Method begins at RVA 0x2050
// Code size 85 (0x55)
.maxstack 2
.entrypoint
.locals init (
[0] class [mscorlib]System.InvalidOperationException,
[1] class [mscorlib]System.Exception
)
IL_0000: nop
.try
{
IL_0001: nop
IL_0002: ldnull
IL_0003: call void FilterExceptionSample.Program::TestMessage(string)
IL_0008: nop
IL_0009: nop
IL_000a: leave.s IL_004e
} // end .try
filter
{
IL_000c: isinst [mscorlib]System.InvalidOperationException
IL_0011: dup
IL_0012: brtrue.s IL_0018
IL_0014: pop
IL_0015: ldc.i4.0
IL_0016: br.s IL_002c
IL_0018: stloc.0
IL_0019: ldloc.0
IL_001a: callvirt instance string [mscorlib]System.Exception::get_Message()
IL_001f: ldstr "message test can't be"
IL_0024: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0029: ldc.i4.0
IL_002a: cgt.un
IL_002c: endfilter
} // end filter
catch
{
IL_002e: pop
IL_002f: nop
IL_0030: ldstr "Catch filtered exception"
IL_0035: call void [mscorlib]System.Console::WriteLine(string)
IL_003a: nop
IL_003b: nop
IL_003c: leave.s IL_004e
} // end handler
catch [mscorlib]System.Exception
{
IL_003e: stloc.1
IL_003f: nop
IL_0040: ldstr "Catch other exceptions"
IL_0045: call void [mscorlib]System.Console::WriteLine(string)
IL_004a: nop
IL_004b: nop
IL_004c: leave.s IL_004e
} // end handler
IL_004e: call string [mscorlib]System.Console::ReadLine()
IL_0053: pop
IL_0054: ret
} // end of method Program::Main
.method private hidebysig static
void Main (
string[] args
) cil managed
{
// Method begins at RVA 0x2050
// Code size 85 (0x55)
.maxstack 2
.entrypoint
.locals init (
[0] class [mscorlib]System.InvalidOperationException,
[1] class [mscorlib]System.Exception
)
IL_0000: nop
.try
{
IL_0001: nop
IL_0002: ldnull
IL_0003: call void FilterExceptionSample.Program::TestMessage(string)
IL_0008: nop
IL_0009: nop
IL_000a: leave.s IL_004e
} // end .try
filter
{
IL_000c: isinst [mscorlib]System.InvalidOperationException
IL_0011: dup
IL_0012: brtrue.s IL_0018
IL_0014: pop
IL_0015: ldc.i4.0
IL_0016: br.s IL_002c
IL_0018: stloc.0
IL_0019: ldloc.0
IL_001a: callvirt instance string [mscorlib]System.Exception::get_Message()
IL_001f: ldstr "message test can't be"
IL_0024: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0029: ldc.i4.0
IL_002a: cgt.un
IL_002c: endfilter
} // end filter
catch
{
IL_002e: pop
IL_002f: nop
IL_0030: ldstr "Catch filtered exception"
IL_0035: call void [mscorlib]System.Console::WriteLine(string)
IL_003a: nop
IL_003b: nop
IL_003c: leave.s IL_004e
} // end handler
catch [mscorlib]System.Exception
{
IL_003e: stloc.1
IL_003f: nop
IL_0040: ldstr "Catch other exceptions"
IL_0045: call void [mscorlib]System.Console::WriteLine(string)
IL_004a: nop
IL_004b: nop
IL_004c: leave.s IL_004e
} // end handler
IL_004e: call string [mscorlib]System.Console::ReadLine()
IL_0053: pop
IL_0054: ret
} // end of method Program::Main
Чтобы не грузить операторами в IL-коде, просто расскажу, что здесь происходит. У
нас появился блок filter, в котором мы ищем тип exception, который удовлетворяет наше условие (функция isinst).
IL_000c: isinst [mscorlib]System.InvalidOperationException
IL_0011: dup
IL_0012: brtrue.s IL_0018
IL_0011: dup
IL_0012: brtrue.s IL_0018
Затем мы опустим момент игры со стеком, в который мы копируем наш exception, а перейдем к фазе сравнений, в которой мы
проверяем два сообщения на равенство, чтобы определить, что наш фильтр сработал.
IL_0018: stloc.0
IL_0019: ldloc.0
IL_001a: callvirt instance string [mscorlib]System.Exception::get_Message()
IL_001f: ldstr "message test can't be"
IL_0024: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0029: ldc.i4.0
IL_002a: cgt.un
IL_0019: ldloc.0
IL_001a: callvirt instance string [mscorlib]System.Exception::get_Message()
IL_001f: ldstr "message test can't be"
IL_0024: call bool [mscorlib]System.String::op_Equality(string, string)
IL_0029: ldc.i4.0
IL_002a: cgt.un
Здесь код очень простой и примитивный, а если вы откроете его в ILSpy, то сможете увидеть и подсветку этого кода. Ниже
показан результат запуска нашего примера.
Если ваш фильтр не сработает, то будет выполнен следующий поиск
следующего exception, который
удовлетворяет критерии и либо имеет фильтр исключений, либо нет. То есть, вы можете в ряд написать несколько
раз обработку одного и того же типа исключения. Например, код, который позволяет
проверить результат обращения к странице в интернете.
public static void
ProcessResponceCommand()
{
try
{
throw new HttpException (404, "Page not found");
}
catch(HttpException ex) when (ex.GetHttpCode() == 404)
{
WriteLine("Page not found");
}
catch (HttpException ex) when (ex.GetHttpCode() == 403)
{
WriteLine("You must be logged to access this
resource!!!");
}
catch(Exception ex)
{
WriteLine("Other errors");
}
}
А теперь посмотрите, как этот код выглядит на уровне IL-кода.
.method public hidebysig static
void ProcessResponceCommand () cil managed
{
// Method begins at RVA 0x2120
// Code size 129 (0x81)
.maxstack 2
.locals init (
[0] class FilterException.HttpException,
[1] class FilterException.HttpException,
[2] class [mscorlib]System.Exception
)
IL_0000: nop
.try
{
IL_0001: nop
IL_0002: ldc.i4 404
IL_0007: ldstr "Page not found"
IL_000c: newobj instance void FilterException.HttpException::.ctor(int32, string)
IL_0011: throw
} // end .try
filter
{
IL_0012: isinst FilterException.HttpException
IL_0017: dup
IL_0018: brtrue.s IL_001e
IL_001a: pop
IL_001b: ldc.i4.0
IL_001c: br.s IL_002f
IL_001e: stloc.0
IL_001f: ldloc.0
IL_0020: callvirt instance int32 FilterException.HttpException::GetHttpCode()
IL_0025: ldc.i4 404
IL_002a: ceq
IL_002c: ldc.i4.0
IL_002d: cgt.un
IL_002f: endfilter
} // end filter
catch
{
IL_0031: pop
IL_0032: nop
IL_0033: ldstr "Page not found"
IL_0038: call void [mscorlib]System.Console::WriteLine(string)
IL_003d: nop
IL_003e: nop
IL_003f: leave.s IL_0080
} // end handler
filter
{
IL_0041: isinst FilterException.HttpException
IL_0046: dup
IL_0047: brtrue.s IL_004d
IL_0049: pop
IL_004a: ldc.i4.0
IL_004b: br.s IL_005e
IL_004d: stloc.1
IL_004e: ldloc.1
IL_004f: callvirt instance int32 FilterException.HttpException::GetHttpCode()
IL_0054: ldc.i4 403
IL_0059: ceq
IL_005b: ldc.i4.0
IL_005c: cgt.un
IL_005e: endfilter
} // end filter
catch
{
IL_0060: pop
IL_0061: nop
IL_0062: ldstr "You must be logged to access this resource!!!"
IL_0067: call void [mscorlib]System.Console::WriteLine(string)
IL_006c: nop
IL_006d: nop
IL_006e: leave.s IL_0080
} // end handler
catch [mscorlib]System.Exception
{
IL_0070: stloc.2
IL_0071: nop
IL_0072: ldstr "Other errors"
IL_0077: call void [mscorlib]System.Console::WriteLine(string)
IL_007c: nop
IL_007d: nop
IL_007e: leave.s IL_0080
} // end handler
IL_0080: ret
} // end of method Program::ProcessResponceCommand
void ProcessResponceCommand () cil managed
{
// Method begins at RVA 0x2120
// Code size 129 (0x81)
.maxstack 2
.locals init (
[0] class FilterException.HttpException,
[1] class FilterException.HttpException,
[2] class [mscorlib]System.Exception
)
IL_0000: nop
.try
{
IL_0001: nop
IL_0002: ldc.i4 404
IL_0007: ldstr "Page not found"
IL_000c: newobj instance void FilterException.HttpException::.ctor(int32, string)
IL_0011: throw
} // end .try
filter
{
IL_0012: isinst FilterException.HttpException
IL_0017: dup
IL_0018: brtrue.s IL_001e
IL_001a: pop
IL_001b: ldc.i4.0
IL_001c: br.s IL_002f
IL_001e: stloc.0
IL_001f: ldloc.0
IL_0020: callvirt instance int32 FilterException.HttpException::GetHttpCode()
IL_0025: ldc.i4 404
IL_002a: ceq
IL_002c: ldc.i4.0
IL_002d: cgt.un
IL_002f: endfilter
} // end filter
catch
{
IL_0031: pop
IL_0032: nop
IL_0033: ldstr "Page not found"
IL_0038: call void [mscorlib]System.Console::WriteLine(string)
IL_003d: nop
IL_003e: nop
IL_003f: leave.s IL_0080
} // end handler
filter
{
IL_0041: isinst FilterException.HttpException
IL_0046: dup
IL_0047: brtrue.s IL_004d
IL_0049: pop
IL_004a: ldc.i4.0
IL_004b: br.s IL_005e
IL_004d: stloc.1
IL_004e: ldloc.1
IL_004f: callvirt instance int32 FilterException.HttpException::GetHttpCode()
IL_0054: ldc.i4 403
IL_0059: ceq
IL_005b: ldc.i4.0
IL_005c: cgt.un
IL_005e: endfilter
} // end filter
catch
{
IL_0060: pop
IL_0061: nop
IL_0062: ldstr "You must be logged to access this resource!!!"
IL_0067: call void [mscorlib]System.Console::WriteLine(string)
IL_006c: nop
IL_006d: nop
IL_006e: leave.s IL_0080
} // end handler
catch [mscorlib]System.Exception
{
IL_0070: stloc.2
IL_0071: nop
IL_0072: ldstr "Other errors"
IL_0077: call void [mscorlib]System.Console::WriteLine(string)
IL_007c: nop
IL_007d: nop
IL_007e: leave.s IL_0080
} // end handler
IL_0080: ret
} // end of method Program::ProcessResponceCommand
Наш код приобрел следующий вид:
Использовать фильтры исключений удобно в том случае, если нам нужно
выполнить какие-то действия до раскрутки стека. Также очень удобный способ обработки ошибок на
примере для HttpException. Аналогичную
обработку можно сделать для
SqlException, обрабатывая
свойство Number и другие типы ошибок,
которые позволяют произведи анализ ошибки еще до раскрутки стека.
Побочные эффекты фильтров исключений
Есть интересный нюанс, который позволяет логировать ваши ошибки без
раскрутки стека. Для этого нам нужно добавить первым блоком catch, который будет только логировать ошибки и возвращать
false, что означает, что обработка
будет передана дальше другим обработчикам catch. Об этой ситуации описал Мэдс Торгесен в статье 'New Features in C# 6'.
Exception filters are
preferable to catching and rethrowing because they leave the stack unharmed. If
the exception later causes the stack to be dumped, you can see where it originally
came from, rather than just the last place it was rethrown.
It is also a common
and accepted form of “abuse” to use exception filters for side effects; e.g.
logging. They can inspect an exception “flying by” without intercepting its
course. In those cases, the filter will often be a call to a false-returning
helper function which executes the side effects:
Пример использования такого подхода в C# 6 можно посмотреть ниже.
static void Main(string[] args)
{
try
{
TestMessage(null);
}
catch(Exception ex) when (Log(e)) {}
catch (InvalidOperationException ex) when (ex.Message == "message test can't be null")
{
WriteLine("Catch filtered exception");
}
catch (Exception ex)
{
WriteLine("Catch other exceptions");
}
ReadLine();
}
private static bool
Log(Exception e)
{
/*
log it */ ;
return false;
}
Немного дополнения к статье, которые касаются не фильтров исключений, а новой версии языка C#. В предыдущей версии вместо статических классов с длинной записью Console.WriteLine, можно было написать
using System.Console, и все работало без
использования Console. Как это работало
раньше, и как работает сейчас, показано на рисунке ниже.
Итоги
На этом буду завершать краткое использование
фильтров исключений в C# 6.0. Надеюсь,
для себя вы найдете полезной новую возможность языка C# и будете использовать ее в полную силу. В целом,
фильтры исключений – достаточно мощная новая фишка, которая появилась в C#, и внимательное, вдумчивое
использование данной особенности позволят как минимум удобно логировать ваши
ошибки. Например, команда Roslyn и TypeScript использует эту возможность для генерации
более детальных дампов, тогда почему бы также не посмотреть в эту сторону. C# 6.0 принесет нам несколько неплохих возможностей,
так что стремимся узнать о них заранее, чтобы использовать в будущем по
максимуму.
No comments:
Post a Comment