2016-06-09 16 views
36

Załóżmy, że mamwyliczenia „połów”, że nie daj się „gotowe” przez dzwoniącego - Co się dzieje

IEnumerable<string> Foo() 
{ 
    try 
    { 

     /// open a network connection, start reading packets 
     while(moredata) 
     { 
      yield return packet; 
     } 
    } 
    finally 
     { 
     // close connection 
     } 
} 

(lub być może zrobiłem „dzięki” - to samo). Co się stanie, jeśli mój rozmówca poda

var packet = Foo().First(); 

Zostałem właśnie z nieszczelnym połączeniem. Kiedy w końcu się inwokuje? Czy też słuszne zawsze dzieją się magiczne

edit z odpowiedzią i myśli

My próbki i inne „normalne” (foreach, ..) wywołanie wzorców będzie działać dobrze, bo wyrzucać IEnumerable (właściwie IEnumerator zwrócony przez GetEnumerator). Muszę więc mieć gdzieś dzwoniącego, który robi coś fajnego (jawnie pobierającego moduł wyliczający i nie wyrzucającego go itp.). Będę je zastrzelił

zły kod

znalazłem rozmówcę robi

IEnumerator<T> enumerator = foo().GetEnumerator(); 

zmieniona na

using(IEnumerator<T> enumerator = foo().GetEnumerator()) 
+0

Moje najlepsze przypuszczenie: zwraca tylko pierwszy 'pakiet' (?) –

+2

@MaciejLos Który nie odpowiada na pytanie, które zadał. – Servy

+1

@Servy, wiem. To był powód, dla którego napisałem komentarz. Jak widać, dodałem '?' Na końcu instrukcji, bo nie jestem pewien. Dzięki za komentarz. –

Odpowiedz

37

ja właśnie wyszedł z wyciekły połączenia.

Nie, nie jesteś.

Kiedy następuje wywołanie w końcu?

Gdy IEnumerator<T> jest umieszczony, co First zamierza zrobić po otrzymaniu pierwszy element sekwencji (podobnie jak każdy powinien robić kiedy użyć IEnumerator<T>).

Teraz, jeśli ktoś napisał:

//note no `using` block on `iterator` 
var iterator = Foo().GetEnumerator(); 
iterator.MoveNext(); 
var first = iterator.Current; 
//note no disposal of iterator 

następnie będą przeciekać zasób, ale błąd jest w kodzie rozmówcy, a nie bloku iterator.

+0

Ma sens. [Dokumentacja MSDN] (https://msdn.microsoft.com/en-us/library/9k7k7cf0.aspx) stwierdza to samo. –

27

Nie skończyłoby się wyciekiem połączenia. Obiekty Iterator wyprodukowane przez yield returnIDisposable, a funkcje LINQ są ostrożne, aby zapewnić prawidłowe usuwanie.

Na przykład First() jest realizowany w następujący sposób:

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(); 
} 

Zauważ, że wynikiem source.GetEnumerator() jest zawinięty w using. Zapewnia to połączenie z numerem Dispose, co z kolei zapewnia wywołanie twojego kodu w bloku finally.

To samo dotyczy iteracji przy pętli foreach: kod zapewnia wyrzucenie modułu wyliczającego, niezależnie od tego, czy wyliczenie zostanie ukończone, czy nie.

Jedynym przypadkiem, w którym może dojść do wycieku połączenia, jest zadzwonienie pod numer GetEnumerator i niepoprawne pozbycie się go. Jest to jednak błąd w kodzie przy użyciu IEnumerable, a nie w samym IEnumerable.

22

OK, to pytanie może zawierać trochę danych empirycznych.

Korzystanie VS2015 oraz projekt zarysowania, napisałem poniższy kod:

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"); 
    } 
} 

A potem nazwali go dwa sposoby:

foreach (string s in Test()) 
{ 
    System.Diagnostics.Debug.Print(s); 
    if (s == "3") break; 
} 

string f = Test().First(); 

która produkuje następujące dane wyjściowe debugowania

1 
1 
2 
2 
3 
3 
Finally 
Disposed 
1 
Finally 
Disposed 

Jak widać, wykonuje zarówno blok finally i Dispose metoda.

+4

w razie wątpliwości napisz program testowy :-) ty – pm100

+0

@ pm100 Aby dodać więcej informacji: Podałem, że korzystam z kompilatora 2015, ale podejrzewam, że zasady ustalania zakresu w specyfikacji C# wymagają tego zachowania. Podejrzewam, że zakończenie modułu wyliczającego pośrednio działa tak, jak ostatni zwrot zysków był po prostu powrotem i obowiązują wszystkie normalne zasady dotyczące zwrotu. – theB

1

Nie ma żadnej specjalnej magii. Jeśli sprawdzisz dokument pod numerem IEnumerator<T>, okaże się, że dziedziczy on po IDisposable. Konstrukt foreach, jak wiecie, jest cukrem syntaktycznym, który jest rozkładany przez kompilator na sekwencję operacji na module wyliczającym, a całość jest zawijana do bloku, wywołując w obiekcie enumeratora obiekt.

Gdy kompilator zamienia metodę iterator (tj. Np. Metoda zawierająca yield oświadczenia) do implementacji IEnumerable<T>/IEnumerator<T>, obsługuje logikę w metodzie wygenerowanej klasy Disposetry/finally.

Możesz spróbować użyć ILDASM do analizy kodu wygenerowanego w twoim przypadku. To będzie dość skomplikowane, ale da ci pomysł.