La connessione al database sarà chiusa se restituiamo la riga datareader e non leggiamo tutti i record?

12

Pur comprendendo come funziona la parola chiave yield , mi sono imbattuto in link1 e link2 su StackOverflow che promuove l'uso di yield return durante l'iterazione su DataReader e soddisfa anche le mie esigenze. Ma mi chiedo cosa succede, se uso yield return come mostrato di seguito e se non faccio scorrere l'intero DataReader, la connessione DB rimarrà aperta per sempre?

IEnumerable<IDataRecord> GetRecords()
{
    SqlConnection myConnection = new SqlConnection(@"...");
    SqlCommand myCommand = new SqlCommand(@"...", myConnection);
    myCommand.CommandType = System.Data.CommandType.Text;
    myConnection.Open();
    myReader = myCommand.ExecuteReader(CommandBehavior.CloseConnection);
    try
       {               
          while (myReader.Read())
          {
            yield return myReader;          
          }
        }
    finally
        {
            myReader.Close();
        }
}


void AnotherMethod()
{
    foreach(var rec in GetRecords())
    {
       i++;
       System.Console.WriteLine(rec.GetString(1));
       if (i == 5)
         break;
  }
}

Ho provato lo stesso esempio in un'app Console di esempio e ho notato durante il debug che il blocco finally di GetRecords() non è stato eseguito. Come posso garantire la chiusura della connessione DB? C'è un modo migliore rispetto all'utilizzo della parola chiave yield ? Sto cercando di progettare una classe personalizzata che sarà responsabile dell'esecuzione di SQL selezionati e stored procedure su DB e restituirà il risultato. Ma non voglio restituire il DataReader al chiamante. Inoltre, voglio assicurarmi che la connessione venga chiusa in tutti gli scenari.

Modifica Modificata la risposta alla risposta di Ben in quanto non è corretto aspettarsi che i chiamanti del metodo utilizzino il metodo correttamente e rispetto alla connessione DB sarà più costoso se il metodo è chiamato più volte per no ragione.

Grazie Jakob e Ben per la spiegazione dettagliata.

    
posta GawdePrasad 19.10.2015 - 11:57
fonte

4 risposte

11

Sì, dovrai affrontare il problema che hai descritto: finché non finisci di ripetere il risultato, manterrai aperta la connessione. Ci sono due approcci generali a cui posso pensare per affrontare questo:

Spingi, non tirare

Attualmente stai restituendo un IEnumerable<IDataRecord> , una struttura dati da cui puoi effettuare il pull. Invece, potresti passare il tuo metodo a spingere i suoi risultati. Il modo più semplice sarebbe passare un Action<IDataRecord> che viene chiamato su ogni iterazione:

void GetRecords(Action<IDataRecord> callback)
{
    // ...
      while (myReader.Read())
      {
        callback(myReader);
      }
}

Nota che, dato che hai a che fare con una raccolta di elementi, IObservable / IObserver potrebbe essere una struttura dati leggermente più appropriata, ma a meno che non ne abbia bisogno, un semplice Action è molto più semplice.

Valutare con entusiasmo

Un'alternativa è solo per assicurarsi che l'iterazione sia completata completamente prima di tornare.

Normalmente puoi farlo semplicemente mettendo i risultati in una lista e poi restituendoli, ma in questo caso c'è la complicazione aggiuntiva di ciascun articolo che è lo stesso riferimento al lettore. Quindi hai bisogno di qualcosa per estrarre il risultato che ti serve dal lettore:

IEnumerable<T> GetRecords<T>(Func<IDataRecord,T> extractor)
{
    // ...
     var result = new List<T>();
     try
     {
       while (myReader.Read())
       {
         result.Add(extractor(myReader));         
       }
     }
     finally
     {
         myReader.Close();
     }
     return result;
}
    
risposta data 19.10.2015 - 14:12
fonte
10

Il tuo blocco finally verrà sempre eseguito.

Quando usi yield return il compilatore creerà una nuova classe nidificata per implementare una macchina a stati.

Questa classe conterrà tutto il codice da blocchi finally come metodi separati. Tiene traccia dei blocchi finally che devono essere eseguiti a seconda dello stato. Tutti i blocchi finally necessari verranno eseguiti nel metodo Dispose .

In base alle specifiche del linguaggio C #, foreach (V v in x) embedded-statement è espanso su

{
    E e = ((C)(x)).GetEnumerator();
    try
    {
        while (e.MoveNext())
        {
            V v = (V)(T)e.Current;
            embedded - statement
        }
    }
    finally {
        … // Dispose e
    }
}

quindi l'enumeratore verrà eliminato anche se esci dal ciclo con break o return .

Per ulteriori informazioni sull'implementazione di iterator, leggi questo questo articolo di Jon Skeet

Modifica

Il problema con questo approccio è che ti affidi ai clienti della tua classe per usare correttamente il metodo. Potrebbero ad esempio ottenere direttamente l'enumeratore e iterarlo attraverso un ciclo while senza eliminarlo.

Dovresti prendere in considerazione una delle soluzioni suggerite da @BenAaronson.

    
risposta data 19.10.2015 - 13:45
fonte
3

Indipendentemente dallo specifico comportamento di yield , il tuo codice contiene percorsi di esecuzione che non disporranno correttamente delle risorse. Cosa succede se la tua seconda linea lancia un'eccezione o la tua terza? O anche il tuo quarto? Avrai bisogno di una catena try / finally molto complessa, oppure puoi usare using blocks.

IEnumerable<IDataRecord> GetRecords()
{
    using(var connection = new SqlConnection(@"..."))
    {
        connection.Open();

        using(var command = new SqlCommand(@"...", connection);
        {
            using(var reader = command.ExecuteReader())
            {
               while(reader.Read())
               {
                   // your code here.
                   yield return reader;
               }
            }
        }
    }
}

Le persone hanno osservato che questo non è intuitivo, che le persone che chiamano il tuo metodo potrebbero non sapere che l'enumerazione del risultato due volte chiamerà il metodo due volte. Bene, sfortuna. È così che funziona la lingua. Questo è il segnale che IEnumerable<T> invia. C'è un motivo per cui questo non restituisce List<T> o T[] . Le persone che non lo sanno devono essere educate, non lavorate.

Visual Studio ha una funzionalità chiamata analisi del codice statico. Puoi utilizzarlo per scoprire se hai disposto correttamente le risorse.

    
risposta data 19.10.2015 - 16:59
fonte
2

Ciò che hai fatto sembra sbagliato, ma funzionerà bene.

Dovresti utilizzare un IEnumerator < > , poiché eredita anche IDisposable .

Ma poiché stai utilizzando un foreach, il compilatore utilizza ancora un IDisposable per generare un IEnumerator < > per il foreach.

In realtà, il foreach implica molte cose interne.

Controlla link

    
risposta data 19.10.2015 - 13:35
fonte

Leggi altre domande sui tag