Domanda

Background: ho un sacco di stringhe che sto ricevendo da un database e voglio restituirle. Tradizionalmente, sarebbe qualcosa del genere:

public List<string> GetStuff(string connectionString)
{
    List<string> categoryList = new List<string>();
    using (SqlConnection sqlConnection = new SqlConnection(connectionString))
    {
        string commandText = "GetStuff";
        using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection))
        {
            sqlCommand.CommandType = CommandType.StoredProcedure;

            sqlConnection.Open();
            SqlDataReader sqlDataReader = sqlCommand.ExecuteReader();
            while (sqlDataReader.Read())
            {
                categoryList.Add(sqlDataReader["myImportantColumn"].ToString());
            }
        }
    }
    return categoryList;
}

Ma poi immagino che il consumatore vorrà scorrere gli articoli e non si preoccupa di molto altro, e mi piacerebbe non inserirmi in un Elenco di per sé, quindi se restituisco un IEnumerable tutto è buono / flessibile. Quindi stavo pensando di poter usare un & Quot; rendimento return & Quot; digitare design per gestire questo ... qualcosa del genere:

public IEnumerable<string> GetStuff(string connectionString)
{
    using (SqlConnection sqlConnection = new SqlConnection(connectionString))
    {
        string commandText = "GetStuff";
        using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection))
        {
            sqlCommand.CommandType = CommandType.StoredProcedure;

            sqlConnection.Open();
            SqlDataReader sqlDataReader = sqlCommand.ExecuteReader();
            while (sqlDataReader.Read())
            {
                yield return sqlDataReader["myImportantColumn"].ToString();
            }
        }
    }
}

Ma ora che sto leggendo un po 'di più sulla resa (su siti come questo ... msdn non sembra menzionare questo), è apparentemente un valutatore pigro, che tiene in considerazione lo stato del popolatore, in anticipo di qualcuno che chiede il valore successivo e quindi lo esegue solo fino a quando non restituisce il valore successivo.

Questo sembra andare bene nella maggior parte dei casi, ma con una chiamata DB, sembra un po 'rischioso. Come esempio un po 'inventato, se qualcuno chiede un IEnumerable da cui sto popolando da una chiamata DB, ne supera la metà e poi si blocca in un ciclo ... per quanto posso vedere la mia connessione DB sta andando per rimanere aperto per sempre.

Sembra in alcuni casi chiedere problemi se l'iteratore non finisce ... mi sto perdendo qualcosa?

È stato utile?

Soluzione

È un atto di bilanciamento: vuoi forzare immediatamente tutti i dati in memoria in modo da poter liberare la connessione, o vuoi beneficiare dello streaming dei dati, a costo di legare la connessione per tutto quel tempo ?

Per come la vedo io, quella decisione dovrebbe potenzialmente spettare al chiamante, che sa di più su ciò che vuole fare. Se scrivi il codice usando un blocco iteratore, il chiamante può molto trasformare facilmente quel modulo di streaming in un modulo con buffer completo:

List<string> stuff = new List<string>(GetStuff(connectionString));

Se invece esegui il buffering tu stesso, non è possibile che il chiamante torni a un modello di streaming.

Quindi probabilmente userò il modello di streaming e direi esplicitamente nella documentazione cosa fa, e consiglio al chiamante di decidere in modo appropriato. Potresti anche voler fornire un metodo di supporto per chiamare sostanzialmente la versione in streaming e convertirla in un elenco.

Naturalmente, se non ti fidi dei chiamanti per prendere la decisione appropriata e hai buone ragioni per credere che non vorranno mai davvero trasmettere i dati (ad esempio, non restituiranno mai molto comunque) quindi vai per l'approccio elenco. Ad ogni modo, documentalo - potrebbe influenzare molto bene come viene usato il valore di ritorno.

Un'altra opzione per gestire grandi quantità di dati è quella di utilizzare i batch, ovviamente - questo è un po 'lontano dalla domanda originale, ma è un approccio diverso da considerare nella situazione in cui lo streaming sarebbe normalmente attraente.

Altri suggerimenti

Non sei sempre pericoloso con IEnumerable. Se lasci la chiamata quadro GetEnumerator (che è ciò che farà la maggior parte delle persone), allora sei al sicuro. Fondamentalmente, sei al sicuro quanto la prudenza del codice usando il tuo metodo:

class Program
{
    static void Main(string[] args)
    {
        // safe
        var firstOnly = GetList().First();

        // safe
        foreach (var item in GetList())
        {
            if(item == "2")
                break;
        }

        // safe
        using (var enumerator = GetList().GetEnumerator())
        {
            for (int i = 0; i < 2; i++)
            {
                enumerator.MoveNext();
            }
        }

        // unsafe
        var enumerator2 = GetList().GetEnumerator();

        for (int i = 0; i < 2; i++)
        {
            enumerator2.MoveNext();
        }
    }

    static IEnumerable<string> GetList()
    {
        using (new Test())
        {
            yield return "1";
            yield return "2";
            yield return "3";
        }
    }

}

class Test : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("dispose called");
    }
}

La possibilità di lasciare aperta o meno la connessione al database dipende dalla propria architettura. Se il chiamante partecipa a una transazione (e la tua connessione viene automaticamente inclusa), la connessione verrà comunque mantenuta aperta dal framework.

Un altro vantaggio di yield è (quando si utilizza un cursore lato server), il codice non deve leggere tutti i dati (esempio: 1.000 elementi) dal database, se il consumatore desidera uscire dal ciclo precedente (esempio: dopo il decimo oggetto). Ciò può accelerare l'interrogazione dei dati. Soprattutto in un ambiente Oracle, in cui i cursori sul lato server sono il modo comune per recuperare i dati.

Non ti manca nulla. Il tuo esempio mostra come NON utilizzare il rendimento. Aggiungi gli elementi a un elenco, chiudi la connessione e restituisci l'elenco. La firma del metodo può comunque restituire IEnumerable.

Modifica: Detto questo, Jon ha ragione (così sorpreso!): ci sono rare occasioni in cui lo streaming è in realtà la cosa migliore da fare dal punto di vista delle prestazioni. Dopotutto, se sono 100.000 (1.000.000? 10.000.000?) Righe di cui stiamo parlando qui, non vorrai prima caricare tutto questo in memoria.

Per inciso, nota che l'approccio IEnumerable < T > è essenzialmente che cosa fanno i provider LINQ (LINQ-to-SQL, LINQ-to-Entities) una vita. L'approccio ha dei vantaggi, come dice Jon. Tuttavia, ci sono anche problemi definiti - in particolare (per me) in termini di (la combinazione di) separazione | astrazione.

Quello che intendo qui è che:

  • in uno scenario MVC (ad esempio) vuoi che i tuoi " get data " passare a effettivamente ottenere i dati , in modo da poterli testare sul controller , non sulla vista (senza ricordare di aver chiamato .ToList () ecc)
  • non è possibile garantire che un'altra implementazione DAL sia in grado di trasmettere i dati in streaming (ad esempio, una chiamata POX / WSE / SOAP di solito non può trasmettere in streaming i record); e non vuoi necessariamente rendere il comportamento confusamente diverso (ad es. connessione ancora aperta durante l'iterazione con un'implementazione e chiusa per un'altra)

Questo si lega un po 'ai miei pensieri qui: Pragmatic LINQ .

Ma dovrei sottolineare: ci sono sicuramente momenti in cui lo streaming è altamente desiderabile. Non è semplice " sempre vs mai " cosa ...

Modo leggermente più conciso per forzare la valutazione dell'iteratore:

using System.Linq;

//...

var stuff = GetStuff(connectionString).ToList();

No, sei sulla strada giusta ... la resa bloccherà il lettore ... puoi provarlo facendo un'altra chiamata al database mentre chiami IEnumerable

L'unico modo in cui ciò potrebbe causare problemi è se il chiamante abusa del protocollo di IEnumerable < T > . Il modo corretto di usarlo è chiamare Dispose quando non è più necessario.

L'implementazione generata da yield return prende la chiamata Dispose come segnale per eseguire qualsiasi blocco finalmente aperto, che nel tuo esempio chiamerà Dispose sugli oggetti che hai creato nelle istruzioni usando .

Esistono diverse funzionalità linguistiche (in particolare foreach ) che rendono molto facile usare IEnumerable < T > correttamente.

Puoi sempre usare un thread separato per bufferizzare i dati (forse in una coda) mentre fai anche un tentativo per restituire i dati. Quando l'utente richiede i dati (restituiti tramite un anno), un elemento viene rimosso dalla coda. Inoltre, i dati vengono continuamente aggiunti alla coda tramite il thread separato. In questo modo, se l'utente richiede i dati abbastanza velocemente, la coda non è mai molto piena e non devi preoccuparti di problemi di memoria. In caso contrario, la coda si riempirà, il che potrebbe non essere così male. Se esiste una sorta di limitazione che si desidera imporre in memoria, è possibile applicare una dimensione massima della coda (a quel punto l'altro thread aspetterebbe che gli elementi vengano rimossi prima di aggiungerne altri alla coda). Naturalmente, dovrai assicurarti di gestire correttamente le risorse (ovvero la coda) tra i due thread.

In alternativa, potresti forzare l'utente a passare un valore booleano per indicare se i dati devono essere bufferizzati o meno. Se vero, i dati vengono bufferizzati e la connessione viene chiusa il prima possibile. Se falso, i dati non sono bufferizzati e la connessione al database rimane aperta fintanto che l'utente ne ha bisogno. Avere un parametro booleano obbliga l'utente a fare la scelta, il che assicura che sia a conoscenza del problema.

Mi sono imbattuto in questo muro alcune volte. Le query del database SQL non sono facilmente streaming come i file. Invece, esegui una query solo quanto ritieni necessario e restituiscilo come qualsiasi contenitore desideri ( IList < > , DataTable , ecc.). IEnumerable non ti aiuterà qui.

Quello che puoi fare è usare un SqlDataAdapter e riempire una DataTable. Qualcosa del genere:

public IEnumerable<string> GetStuff(string connectionString)
{
    DataTable table = new DataTable();
    using (SqlConnection sqlConnection = new SqlConnection(connectionString))
    {
        string commandText = "GetStuff";
        using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection))
        {
            sqlCommand.CommandType = CommandType.StoredProcedure;
            SqlDataAdapter dataAdapter = new SqlDataAdapter(sqlCommand);
            dataAdapter.Fill(table);
        }

    }
    foreach(DataRow row in table.Rows)
    {
        yield return row["myImportantColumn"].ToString();
    }
}

In questo modo, stai interrogando tutto in un colpo solo e chiudi immediatamente la connessione, ma stai ancora pigramente ripetendo il risultato. Inoltre, il chiamante di questo metodo non può trasmettere il risultato a un elenco e fare qualcosa che non dovrebbe fare.

Non utilizzare la resa qui. il tuo campione va bene.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top