Как стать автором
Обновить

Комментарии 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#. Когда ваш код или используемая вами библиотека вызывает какую-то системную функцию (которая почти наверняка написана на C или C++), эта функция будет использовать ровно тот же самый стек.

Вы никогда не знаете, сколько памяти понадобится какой-нибудь функции, что вы вызываете. Если где-то там встретится небольшая рекурсия, так как разработчики посчитали, что в стеке будет достаточно свободно, а вы оставили совсем немного свободного пространства, ваша программа просто упадёт. И ради чего? Просто выделяйте любые достаточно большие буферы в куче. Тем более, что C# не про хардкорные оптимизации в стиле C++ вообще (хоть там и есть stackalloc и даже указатели в unsafe коде для очень специальных случаев).

Небольшое замечание по фразе «достаточно места на стеке для средней функции» - average function лучше как "типичная функция" переводить. Тогда понятнее становится: «достаточно места на стеке для типичной функции»

Зарегистрируйтесь на Хабре, чтобы оставить комментарий