Подтвердить что ты не робот

"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())
4b9b3361

Ответ 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 для анализа кода, созданного в вашем случае. Это будет довольно сложно, но это даст вам эту идею.