2009-09-10 8 views
19

Esiste un modo corretto per interrompere una foreach in modo che l'IEnumerable <> sappia che ho finito e che dovrebbe essere ripulito.una preoccupazione per rendimento e rottura da una foreach

Si consideri il seguente codice:

private static IEnumerable<Person> getPeople() 
    { 
     using (SqlConnection sqlConnection = new SqlConnection("...")) 
     { 
      try 
      { 
       sqlConnection.Open(); 
       using (SqlCommand sqlCommand = new SqlCommand("select id, firstName, lastName from people", sqlConnection)) 
       { 

        using (SqlDataReader reader = sqlCommand.ExecuteReader()) 
        { 
         while (reader.Read()) 
          yield return new Person(reader.GetGuid(0), reader.GetString(1), reader.GetString(2)); 
        } 
       } 
      } 
      finally 
      { 
       Console.WriteLine("finally disposing of the connection"); 
       if (sqlConnection.State == System.Data.ConnectionState.Open) 
        sqlConnection.Close(); 
      } 
     } 
    } 

Se consumatore non si rompe dalla foreach allora everthing va bene e il lettore tornerà falsa, il ciclo willend po 'e la funzione pulisce il comando di database e il collegamento . Ma cosa succede se il chiamante si interrompe dal foreach prima che io abbia finito?

+1

vedi anche http://stackoverflow.com/questions/1400146/are-there-any-pitfalls-to-using-an-ienumerablet-return-type-for-sql-data/1400195# 1400195 –

risposta

30

Ottima domanda. Non devi preoccuparti di questo; il compilatore si prende cura di te per te. Fondamentalmente, ciò che facciamo è mettere il codice cleanup per i blocchi finally in un metodo di cleanup speciale sull'iteratore generato. Quando il controllo lascia il blocco foreach del chiamante, il compilatore genera un codice che chiama il codice di pulizia sull'iteratore.

Un esempio semplificato:

static IEnumerable<int> GetInts() 
{ 
    try { yield return 1; yield return 2;} 
    finally { Cleanup(); } 
} 

La tua domanda è fondamentalmente "Is Cleanup() chiamato in questo scenario?"

foreach(int i in GetInts()) { break; } 

Sì. Il blocco iteratore viene generato come una classe con un metodo Dispose che chiama Cleanup, e poi il ciclo foreach viene generato come qualcosa di simile a:

{ 
    IEnumerator<int> enumtor = GetInts().GetEnumerator(); 
    try 
    { 
    while(enumtor.MoveNext()) 
    { 
     i = enumtor.Current; 
     break; 
    } 
    } 
    finally 
    { 
    enumtor.Dispose(); 
    } 
} 

Così, quando la rottura accade, il finalmente prende il sopravvento e il dissipatore si chiama .

Vedere la mia recente serie di articoli se si desiderano ulteriori informazioni su alcuni dei casi angolari strani che abbiamo considerato nella progettazione di questa funzionalità.

http://blogs.msdn.com/ericlippert/archive/tags/Iterators/default.aspx

+0

Questa è un'ottima risposta! –

2

È possibile utilizzare l'istruzione

yield break; 

per uscire da un ciclo resa presto, ma il codice rivela un malinteso penso ... Quando si utilizza il "tramite" l'istruzione,

using (SqlConnection sqlConnection = new SqlConnection("...")) 
{ 
    // other stuff 
} 

automaticamente ottiene una prova blocco finally nel codice iL compilato, e il blocco finnaly chiamerà da smaltire, e nel codice Smaltire la connessione verrà chiusa ...

+0

Correzione minima: "interruzione di rendimento" è una dichiarazione. – jason

+0

Modificato per correggere –

2

Vediamo se ottengo la tua domanda.

foreach(Person p in getPeople()) 
{ 
    // break here 
} 

causa della parola chiave foreach, l'enumeratore sia correttamente disposto. Durante la disposizione di Enumerator, l'esecuzione di getPeople() viene interrotta. Quindi la connessione viene pulita correttamente.

Problemi correlati