2016-06-09 9 views
36

supponiamo di avereenumerazioni 'resa' che non vengono 'finito' dal chiamante - cosa succede

IEnumerable<string> Foo() 
{ 
    try 
    { 

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

(O forse ho fatto un 'usando' - stessa cosa). Cosa succede se il mio chiamante va

var packet = Foo().First(); 

Sono appena uscito con una connessione trapelata. Quando viene finalmente richiamato? Oppure la cosa giusta sempre accade per magia

di modifica con risposta e pensieri

mio campione e altre 'normale' (foreach, ..) chiamando i modelli funzionano bene, perché dispongono del IEnumerable (in realtà l'IEnumerator restituito da GetEnumerator). Devo quindi avere un chiamante da qualche parte che sta facendo qualcosa di funky (ottenendo esplicitamente un enumeratore e non smaltendolo o qualcosa di simile). Avrò li fucilarono

il codice cattivo

ho trovato un chiamante facendo

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

cambiato in

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

La mia ipotesi migliore: restituisce solo il primo 'pacchetto '(?) –

+2

@MaciejLos Questo non risponde alla domanda che ha posto. – Servy

+1

@Servy, lo so. Questa è stata la ragione per cui ho postato un commento. Come puoi vedere, ho aggiunto "?" Alla fine dell'istruzione, perché non ne sono sicuro. Grazie per il tuo commento. –

risposta

37

sto appena lasciato con una connessione trapelato.

No, non lo sei.

Quando viene infine richiamato?

Quando il IEnumerator<T> è disposto, che First ha intenzione di fare dopo aver ottenuto il primo elemento della sequenza (proprio come tutti dovrebbero fare quando usano un IEnumerator<T>).

Ora, se qualcuno ha scritto:

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

poi avrebbero perdita di risorsa, ma c'è il bug è nel codice chiamante, non il blocco iteratore.

+0

Ha senso. [Documentazione MSDN] (https://msdn.microsoft.com/en-us/library/9k7k7cf0.aspx) afferma lo stesso. –

27

Non si finirebbe con la connessione trapelata. Gli oggetti Iterator prodotti da yield return sono IDisposable e le funzioni LINQ sono attente a garantire un corretto smaltimento.

Ad esempio, First() viene effettuata nel modo seguente:

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

noti come il risultato di source.GetEnumerator() è avvolto in using. Questo assicura la chiamata a Dispose, che a sua volta assicura la chiamata del codice nel blocco finally.

Lo stesso vale per le iterazioni tramite il ciclo foreach: il codice garantisce l'eliminazione dell'enumeratore indipendentemente dal completamento dell'enumerazione.

L'unico caso in cui si può finire con una connessione trapelata è quando si chiama GetEnumerator da soli e non riescono a smaltirlo correttamente. Tuttavia, si tratta di un errore nel codice che utilizza IEnumerable, non nello stesso IEnumerable.

22

Ok questa domanda potrebbe utilizzare un po 'di dati empirici.

Utilizzando VS2015 e un progetto ex novo, ho scritto il seguente codice:

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

E poi chiamato in due modi:

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

string f = Test().First(); 

che produce il seguente output di debug

1 
1 
2 
2 
3 
3 
Finally 
Disposed 
1 
Finally 
Disposed 

Come possiamo vedere, esegue sia il blocco finally che loMetodo.

+4

in caso di dubbio scrivere un programma di test :-) ty – pm100

+0

@ pm100 Per aggiungere ulteriori informazioni: Ho specificato che stavo usando il compilatore 2015, ma ho il sospetto che le regole di scoping nelle specifiche C# richiedano questo comportamento. Il mio sospetto è che la terminazione di un enumeratore implichi implicitamente come l'ultimo return return fosse semplicemente un ritorno e si applicano tutte le normali regole di ritorno. – theB

1

Non c'è magia speciale. Se controlli il documento su IEnumerator<T>, scoprirai che eredita da IDisposable. Il costrutto foreach, come sapete, è zucchero sintattico che viene decomposto dal compilatore in una sequenza di operazioni su un enumeratore e l'intera operazione viene avvolta in un blocco try/finally, chiamando lo Dispose sull'oggetto enumeratore.

Quando il compilatore converte un metodo iterator (metodo i. E., Contenente yield istruzioni) in un'implementazione di IEnumerable<T>/IEnumerator<T>, gestisce la logica try/finally nel metodo della classe generata Dispose.

Si potrebbe provare a utilizzare ILDASM per analizzare il codice generato nel tuo caso. Sarà piuttosto complesso ma ti darà l'idea.

Problemi correlati