День сто девяносто шестой. #ЗаметкиНаПолях

Обработка блоков 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.