IEnumerator<T>
реализует IDisposable
, и циклы foreach
будут располагать то, что они перечисляют, когда они закончат (это включает методы linq, которые используют цикл foreach
, такой как .ToArray()
).
Оказывается, что конечный автомат, сгенерированный компилятором для методов генератора, реализует умный способ Dispose
: если конечный автомат находится в состоянии, которое находится «внутри» блока using
, тогда вызывается Dispose()
на конечном автомате будет располагать объект, защищенный оператором using
.
Давайте рассмотрим пример:
public IEnumerable<string> M() {
yield return "1";
using (var ms = new MemoryStream())
{
yield return "2";
yield return "3";
}
yield return "4";
}
Я не собираюсь вставлять весь текстгенерируется конечный автомат, так как он очень большой. Вы можете увидеть это в SharpLab здесь .
Ядром конечного автомата является следующий оператор switch, который отслеживает наш прогресс после каждого из операторов yield return
:
switch (<>1__state)
{
default:
return false;
case 0:
<>1__state = -1;
<>2__current = "1";
<>1__state = 1;
return true;
case 1:
<>1__state = -1;
<ms>5__1 = new MemoryStream();
<>1__state = -3;
<>2__current = "2";
<>1__state = 2;
return true;
case 2:
<>1__state = -3;
<>2__current = "3";
<>1__state = 3;
return true;
case 3:
<>1__state = -3;
<>m__Finally1();
<ms>5__1 = null;
<>2__current = "4";
<>1__state = 4;
return true;
case 4:
<>1__state = -1;
return false;
}
Вы можете видеть, что мы создаем MemoryStream
при входе в состояние 2 и удаляем его (вызывая <>m__Finally1()
) при выходе из состояния 3.
Вот метод Dispose
:
void IDisposable.Dispose()
{
int num = <>1__state;
if (num == -3 || (uint)(num - 2) <= 1u)
{
try
{
}
finally
{
<>m__Finally1();
}
}
}
Если мы находимся в состояниях -3, 2 или 3, тогда мы назовем <>m__Finally1();
. Состояния 2 и 3 - это те, которые находятся внутри блока using
.
(Состояние -3 кажется защитником в случае, если мы написали yield return Foo()
и Foo()
сгенерировали исключение: в этом случае мы бы остались всостояние -3 и мы не сможем продолжить итерацию далее. Однако нам все еще разрешено использовать MemoryStream
в этом случае).
Просто для полноты <>m__Finally1
определяется как:
private void <>m__Finally1()
{
<>1__state = -1;
if (<ms>5__1 != null)
{
((IDisposable)<ms>5__1).Dispose();
}
}
Спецификацию для этого можно найти в C # Language Specification , раздел 10.14.4.3:
- Если состояние объекта перечислителяприостанавливается, вызывая Dispose:
- Изменяет состояние на рабочее.
- Выполняет любые блоки finally, как если бы последний выполненный оператор yield return был оператором yield break. Если это вызывает исключение, которое выдается и распространяется из тела итератора, состояние объекта-перечислителя устанавливается равным after, а исключение распространяется на вызывающую функцию метода Dispose.
- Изменяет состояние на after.