День сто девяносто шестой. #ЗаметкиНаПолях
Обработка блоков finally в итераторах
Рассмотрим следующий код:
- Если считать выполнение прерываемым на каждом вызове
- Если считать, что на самом деле на каждом
Выполним этот код:
На самом деле, и да, и нет. Если реализовать итератор вручную и вызвать
Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 2.
Обработка блоков finally в итераторах
Рассмотрим следующий код:
static IEnumerable<string> Iterator()Когда будет выполнен блок
{
try
{
Console.WriteLine("Перед первым yield");
yield return "первый";
Console.WriteLine("После первого yield");
yield return "второй";
Console.WriteLine("После второго yield");
}
finally
{
Console.WriteLine("Внутри finally");
}
}
finally
: - Если считать выполнение прерываемым на каждом вызове
yield return
, тогда логически они внутри блока try
, и нет надобности выполнять блок finally
каждый раз.- Если считать, что на самом деле на каждом
yield return
вызывается метод MoveNext()
итератора, то можно решить, что происходит выход из try
, и тогда finally
должен выполняться.Выполним этот код:
foreach (string value in Iterator())Вывод:
{
Console.WriteLine("Значение: {0}", value);
}
Перед первым yieldТаким образом, блок
Значение: первый
После первого yield
Значение: второй
После второго yield
Внутри finally
finally
выполнится только после окончания итерации. Это подходит под концепцию ленивого выполнения. Пока ничего сложного. Но что если мы прервём итерацию после первого значения? Выполнится ли блок finally
?На самом деле, и да, и нет. Если реализовать итератор вручную и вызвать
MoveNext()
один раз, то блок finally
действительно никогда не будет выполнен. Однако, если использовать итератор внутри цикла foreach
, как это чаще всего происходит, то компилятор использует скрытый блок using
вокруг цикла. При выходе из блока using
происходит вызов метода Dispose()
итератора, и, соответственно, вызов всех блоков finally
. Таким образом, результатом выполнения следующего кода:foreach (string value in Iterator())будет:
{
Console.WriteLine("Значение: {0}", value);
break;
}
Перед первым yieldЭто важно при итерации по объектам, которые требуют уничтожения, таким как обработчики файлов, для предотвращения утечки ресурсов. То есть, если вы в цикле итератора читаете все строки файла, то, даже если вы прерываете цикл на середине, либо в середине цикла возникнет ошибка, файл в любом случае будет корректно закрыт.
Значение: первый
Внутри finally
Источник: Jon Skeet “C# In Depth”. 4th ed – Manning Publications Co, 2019. Глава 2.