Комментарии 19
Третий случай try-finally это foreach. Сторонние библиотеки это четвертый случай.
Спасибо за замечание, добавлю в статью данный пример
Кстати, try-finally
с foreach
имеет свои особенности.
Допустим, есть метод:
IEnumerable<int> CountDown(int count, int? stopAt = null, int? failAt = null)
{
try
{
while (count > -1)
{
yield return count;
if (count == stopAt)
yield break;
if (count == failAt)
throw new Exception($"Inner failure at {nameof(count)} = {count}");
count--;
}
}
finally
{
Console.WriteLine("Finally!!!");
}
}
Тогда
foreach (var i in CountDown(3))
Console.WriteLine(i);
Даст
3
2
1
0
Finally!!!
CountDown(3, stopAt: 1)
выведет
3
2
1
Finally!!!
А CountDown(3, failAt: 1)
3
2
1
Unhandled exception. System.Exception: Inner failure at count = 1
at TryFinally.Program.CountDown(Int32 count, Nullable`1 stopAt, Nullable`1 failAt)+MoveNext() in C:\Users\Admin\source\repos\TryFinally\TryFinally\Program.cs:line 63
at TryFinally.Program.Main(String[] args) in C:\Users\Admin\source\repos\TryFinally\TryFinally\Program.cs:line 10
Finally!!!
Ранний break
finally
не помеха:
Код
foreach (var i in CountDown(3, failAt: 1))
{
Console.WriteLine(i);
break;
}
3
Finally!!!
Грубо говоря
foreach (var i in CountDown(3))
Console.WriteLine(i);
развернется в
Код
var enumerator = CountDown(3).GetEnumerator();
try
{
int i;
while (enumerator.MoveNext())
{
i = enumerator.Current;
Console.WriteLine(i);
}
}
finally
{
enumerator?.Dispose();
}
и результат будет идентичен foreach
и для CountDown(3).GetEnumerator()
, и для CountDown(3, stopAt: 1).GetEnumerator()
, и для CountDown(3, failAt: 1).GetEnumerator()
, и для break;
после Console.WriteLine(i);
.
Если закомментировать enumerator.Dispose();
Код
var enumerator = CountDown(3, stopAt: 1).GetEnumerator();
try
{
int i;
while (enumerator.MoveNext())
{
i = enumerator.Current;
Console.WriteLine(i);
break;
}
}
finally
{
// enumerator.Dispose();
}
то в выводе получим просто 3
, и можно предположить, что код блока finally
метода CountDown
вызывается в enumerator.Dispose()
.
Но если также закомментировать и break;
, то вывод будет:
3
2
1
Finally!!!
Если открыть сборку с методом с помощью ILSpy, метод примерно выглядит так:
Код
IEnumerable<int> CountDown(int count, int? stopAt = null, int? failAt = null)
{
try
{
while (true)
{
if (count > -1)
{
yield return count;
if (count != stopAt)
{
if (count == failAt)
{
break;
}
count--;
continue;
}
yield break; // <- triggers 'finally'
}
yield break; // <- triggers 'finally'
}
throw new Exception(string.Format("Inner failure at {0} = {1}", "count", count));
}
finally
{
Console.WriteLine("Finally!!!");
}
}
И можно заметить, что код блока finally
метода CountDown
вызывается после yield break;
. Один из них используется явно, а второй добавлен автоматически в конец метода компилятором.
А теперь выполним
var enumerator = CountDown(3, failAt: 1).GetEnumerator();
try
{
int i;
while (enumerator.MoveNext())
{
i = enumerator.Current;
Console.WriteLine(i);
}
}
finally
{
// enumerator.Dispose();
}
и получим
3
2
1
Unhandled exception. System.Exception: Inner failure at count = 1
at TryFinally.Program.CountDown(Int32 count, Nullable`1 stopAt, Nullable`1 failAt)+MoveNext() in C:\Users\Admin\source\repos\TryFinally\TryFinally\Program.cs:line 66
at TryFinally.Program.Main(String[] args) in C:\Users\Admin\source\repos\TryFinally\TryFinally\Program.cs:line 40
Finally!!!
То есть исключение тоже запустило выполнение блока finally
.
Таким образом, при использовании try-finally
с yield return
, блок finally
выполняется:
в
enumerator.Dispose();
после исключения
и после
yield break;
И не выполняется вовсе при невыполнении ни одного из этих условий.
Кстати
var enumerator = CountDown(3, failAt: 1).GetEnumerator();
enumerator.Dispose();
Все равно не запустит finally
. Нужен хотябы один вызов enumerator.MoveNext()
.
Да, действительно, это скорее про особенности yield и того, что генерируется под копотом.
И код действительно нелинеен. Но, к счастью, и не параллелен, и поэтому наглядно пробегается в дебагере. Выглядит занятно. Особенное если это какое-нибудь нагромождение IEnumerable(IEnumerable(IEnumerable)) типа цепочек LINQ.
И про
Да, вот как раз в этом и дело. По-моему, просто пока не будет первого MoveNext() выполнение метода с yield вообще даже не начнется.
верно подмечено.
Поэтому, кстати, рекомендуют отделять проверки аргументов от yield. Иначе можно получить исключение в несовсем ожидаемом месте.
Пример:
Пусть есть такой код:
static object CreateObject(object arg)
{
if (arg == null)
throw new ArgumentNullException(nameof(arg));
return null;
}
static IEnumerable<object> CreateEnumerable(object arg)
{
if (arg == null)
throw new ArgumentNullException(nameof(arg));
yield return null;
}
static void Test(object obj)
{
if (obj is IEnumerable enumerable)
{
var enumerator = enumerable.GetEnumerator();
Console.WriteLine(enumerator.ToString());
Console.WriteLine(enumerator.GetType());
Console.WriteLine(enumerator.GetHashCode());
Console.WriteLine(enumerator.Equals(null));
Console.WriteLine(enumerator.Current ?? "NULL");
enumerator.MoveNext();
}
}
Тогда для Test(CreateObject(null))
вывод будет:
Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'arg')
at TryFinally.Program.CreateObject(Object arg) in C:\Users\Admin\source\repos\TryFinally\TryFinally\Program.cs:line 19
at TryFinally.Program.Main(String[] args) in C:\Users\Admin\source\repos\TryFinally\TryFinally\Program.cs:line 11
Вызов метода Test
не произошел.
А для Test(CreateEnumerable(null))
:
TryFinally.Program+<CreateEnumerable>d__2
TryFinally.Program+<CreateEnumerable>d__2
58225482
False
NULL
Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'arg')
at TryFinally.Program.CreateEnumerable(Object arg)+MoveNext() in C:\Users\Admin\source\repos\TryFinally\TryFinally\Program.cs:line 27
at TryFinally.Program.Test(Object obj) in C:\Users\Admin\source\repos\TryFinally\TryFinally\Program.cs:line 40
at TryFinally.Program.Main(String[] args) in C:\Users\Admin\source\repos\TryFinally\TryFinally\Program.cs:line 11
Метод Test
не только был вызван, но и спокойно обращался к членам enumerator
до вызова enumerator.MoveNext()
.
Также стоит упомянуть об сьедании/игнорировании средой исключений. Например, при выбросе исключения внутри finally после уже сработовшого catch. В таком случае оригинальное исключение будет потерено, и заменится на последнее (из finally).
Спасибо за ценное замечание.
В целом это попадает в общую копилку про подмену исключения как в catch блоке (throw; VS throw new Exception(...)), но менее очевидный случай
Так как я не затрагивал эту тему, то не стал писать и про подмену исключения при выбрасывании исключения из файнали
Для этого используют Capture, что бы получить ExeptionDispatchInfo
Спасибо за дополнение про ThreadAbortException, не стал подробно про него расписывать, так как с ним есть множество интересных и неочевидных способов выстрелить себе в ногу из-за чего его лучше не использовать и из-за чего он не поддерживаетсяс 5-ой верии. И решил просто оставить ссылку на документацию где про это расписано, так как сам он не является основной темой данной статьи.
Про исключение поврежденного состояния есть отдельная глава в статье. Или вы подразумевали что-то конкретное?
Его раньше убрали, в более ранних core версиях его уже не было.
Ещё блок finally в C# можно пропустить вообще без каких-либо исключений, достаточно намудрить с асинхронной стейт машиной. Когда-то приводил пример того, как это можно сделать.
stackalloc int[10000]
Это 40 килобайт. Не стоит выделять так много памяти на стеке, он не резиновый. Максимум килобайт 10-20 на все необходимые буферы в функции, а если нужно больше, то добро пожаловать в кучу. Как пример, у Microsoft в VC++ по умолчанию ругается, если больше 16 килобайт. Не помешало бы такое же предупреждение и в C# добавить для stackalloc. Никогда не знаешь, сколько памяти на стеке понадобится функциям, которые вы вызываете из своей функции, поэтому лучше подходить к этому достаточно консервативно и максимально осторожно.В статье данный размер масива был взят для наглядности примера и простоты получения исключения.
В реальном коде мы с одной стороны не знаем заранее какой размер масива нам понадобится, а с другой выделяя более маленькие масивы мы будем только откладывать данную опасность до следующей ряда вложенных функций.
Именно поэтому при подобном аллоцировании имеет смысл подстраховаться с помощью RuntimeHelper
, также при желании можно добавить в условие проверку на размер выделяемого масива.
Вы никогда не знаете, сколько памяти понадобится какой-нибудь функции, что вы вызываете. Если где-то там встретится небольшая рекурсия, так как разработчики посчитали, что в стеке будет достаточно свободно, а вы оставили совсем немного свободного пространства, ваша программа просто упадёт. И ради чего? Просто выделяйте любые достаточно большие буферы в куче. Тем более, что C# не про хардкорные оптимизации в стиле C++ вообще (хоть там и есть stackalloc и даже указатели в unsafe коде для очень специальных случаев).
Небольшое замечание по фразе «достаточно места на стеке для средней функции» - average function лучше как "типичная функция" переводить. Тогда понятнее становится: «достаточно места на стеке для типичной функции»
Исключения среди исключений в .NET