"yield" перечисления, которые не получают "завершены" вызывающим абонентом - что происходит
предположим, что
IEnumerable<string> Foo()
{
try
{
/// open a network connection, start reading packets
while(moredata)
{
yield return packet;
}
}
finally
{
// close connection
}
}
(Или, может быть, я "использовал" - то же самое). Что произойдет, если мой вызывающий абонент отправится
var packet = Foo().First();
Я просто остался с просочившейся связью. Когда наконец вызывается? Или правильная вещь всегда происходит по волшебству
редактировать с ответом и мыслями
Мой образец и другие "нормальные" (foreach,..) вызовы будут работать хорошо, потому что они избавляются от IEnumerable (фактически IEnumerator, возвращаемого GetEnumerator). Поэтому я должен иметь вызывающего абонента, который делает что-то напуганное (явно получая перечислитель, а не удаляя его или тому подобное). Я их расстреляю
плохой код
Я нашел вызывающего пользователя
IEnumerator<T> enumerator = foo().GetEnumerator();
изменено на
using(IEnumerator<T> enumerator = foo().GetEnumerator())
Ответы
Ответ 1
Я просто остался с просочившейся связью.
Нет, это не так.
Когда завершается вызов finally?
Когда IEnumerator<T>
расположен, который First
будет делать после получения первого элемента последовательности (точно так же, как все должны делать, когда они используют IEnumerator<T>
).
Теперь, если кто-то написал:
//note no `using` block on `iterator`
var iterator = Foo().GetEnumerator();
iterator.MoveNext();
var first = iterator.Current;
//note no disposal of iterator
тогда они будут утечка ресурса, но там ошибка в коде вызывающего абонента, а не в блоке итератора.
Ответ 2
У вас не будет просочившегося соединения. Объекты Iterator, создаваемые yield return
, являются IDisposable
, а функции LINQ осторожны, чтобы обеспечить надлежащее удаление.
Например, First()
реализуется следующим образом:
public static TSource First<TSource>(this IEnumerable<TSource> source) {
if (source == null) throw Error.ArgumentNull("source");
IList<TSource> list = source as IList<TSource>;
if (list != null) {
if (list.Count > 0) return list[0];
}
else {
using (IEnumerator<TSource> e = source.GetEnumerator()) {
if (e.MoveNext()) return e.Current;
}
}
throw Error.NoElements();
}
Обратите внимание, как результат source.GetEnumerator()
завернут в using
. Это обеспечивает вызов Dispose
, который, в свою очередь, обеспечивает вызов вашего кода в блоке finally
.
То же самое относится к итерациям цикла foreach
: код обеспечивает удаление перечислителя независимо от того, завершено ли перечисление или нет.
Единственный случай, когда вы можете завершить утечку, - это когда вы вызываете GetEnumerator
самостоятельно и не можете его правильно утилизировать. Однако это ошибка в коде с использованием IEnumerable
, а не только в IEnumerable
.
Ответ 3
Хорошо, этот вопрос мог бы использовать небольшие эмпирические данные.
Используя VS2015 и проект с нуля, я написал следующий код:
private IEnumerable<string> Test()
{
using (TestClass t = new TestClass())
{
try
{
System.Diagnostics.Debug.Print("1");
yield return "1";
System.Diagnostics.Debug.Print("2");
yield return "2";
System.Diagnostics.Debug.Print("3");
yield return "3";
System.Diagnostics.Debug.Print("4");
yield return "4";
}
finally
{
System.Diagnostics.Debug.Print("Finally");
}
}
}
private class TestClass : IDisposable
{
public void Dispose()
{
System.Diagnostics.Debug.Print("Disposed");
}
}
И затем назовем его двумя способами:
foreach (string s in Test())
{
System.Diagnostics.Debug.Print(s);
if (s == "3") break;
}
string f = Test().First();
Что производит следующий отладочный вывод
1
1
2
2
3
3
Finally
Disposed
1
Finally
Disposed
Как мы видим, он выполняет как блок finally
, так и метод Dispose
.
Ответ 4
Нет особой магии. Если вы проверите документ на IEnumerator<T>
, вы обнаружите, что он наследует от IDisposable
. Конструкция foreach
, как вы знаете, представляет собой синтаксический сахар, который разлагается компилятором в последовательность операций над перечислителем, и все это завернуто в блок try
/finally
, вызывая Dispose
на объекте перечислителя.
Когда компилятор преобразует метод итератора (т.е. метод, содержащий инструкции yield
) в реализацию IEnumerable<T>
/IEnumerator<T>
, он обрабатывает логику try
/finally
в методе Dispose
сгенерированный класс.
Вы можете попытаться использовать ILDASM для анализа кода, созданного в вашем случае. Это будет довольно сложно, но это даст вам эту идею.