“Foreach” causa un’esecuzione ripetuta di Linq?

Ho lavorato per la prima volta con Entity Framework in .NET e ho scritto query LINQ per ottenere informazioni dal mio modello. Mi piacerebbe programmare buone abitudini sin dall’inizio, quindi ho fatto ricerche sul modo migliore per scrivere queste domande e ottenere i loro risultati. Sfortunatamente, durante la navigazione in Stack Exchange, ho trovato due spiegazioni contrastanti su come l’esecuzione differita / immediata funzioni con LINQ:

  • Un foreach causa l’esecuzione della query in ogni iterazione del ciclo:

Dimostrato in questione Slow foreach () su una query LINQ – ToList () aumenta enormemente le prestazioni – perché è questo? , l’implicazione è che “ToList ()” deve essere chiamato per valutare immediatamente la query, poiché foreach sta ripetendo la query sull’origine dati ripetutamente, rallentando notevolmente l’operazione.

Un altro esempio è la domanda L’utilizzo di risultati lineari raggruppati è estremamente lento, qualche suggerimento? , dove la risposta accettata implica anche che la chiamata “ToList ()” alla query migliorerà le prestazioni.

  • Un foreach fa sì che una query venga eseguita una sola volta ed è sicura da usare con LINQ

Dimostrato in questione Foreach esegue la query solo una volta? , l’implicazione è che il foreach determina l’enumerazione di un enumerazione e non interrogherà l’origine dati ogni volta.

La continua esplorazione del sito ha sollevato molte domande in cui “l’esecuzione ripetuta durante un ciclo di foreach” è il colpevole del problema delle prestazioni, e molte altre risposte affermano che un foreach approprerà opportunamente una singola query da un’origine dati, il che significa che entrambi le spiegazioni sembrano avere validità. Se l’ipotesi “ToList ()” non è corretta (come la maggior parte delle risposte attuali del 2013-06-05 1:51 PM EST sembrano implicare), da dove viene questo equivoco? C’è una di queste spiegazioni che è accurata e una che non è, o ci sono circostanze diverse che potrebbero causare una query LINQ per valutare in modo diverso?

Modifica: oltre alla risposta accettata di seguito, ho scoperto che la seguente domanda su Programmers ha aiutato molto la mia comprensione dell’esecuzione di query, in particolare le insidie ​​che potrebbero causare più accessi all’origine dati durante un ciclo, che credo essere utile per gli altri interessati a questa domanda: https://softwareengineering.stackexchange.com/questions/178218/for-vs-foreach-vs-linq

In generale LINQ utilizza l’esecuzione differita. Se si utilizzano metodi come First() e FirstOrDefault() la query viene eseguita immediatamente. Quando fai qualcosa del genere;

 foreach(string s in MyObjects.Select(x => x.AStringProp)) 

I risultati vengono recuperati in modo streaming, ovvero uno per uno. Ogni volta che l’iteratore chiama MoveNext la proiezione viene applicata all’object successivo. Se si dovesse avere un punto in Where dovrebbe prima applicare il filtro, quindi la proiezione.

Se fai qualcosa del genere;

 List names = People.Select(x => x.Name).ToList(); foreach (string name in names) 

Quindi credo che questa sia un’operazione dispendiosa. ToList() l’esecuzione della query, enumerando l’elenco People e applicando la proiezione x => x.Name . Successivamente, enumererai di nuovo l’elenco. Quindi, a meno che tu non abbia una buona ragione per avere i dati in una lista (piuttosto che in IEnumerale) stai solo sprecando cicli di CPU.

In generale, l’utilizzo di una query LINQ sulla raccolta che si sta enumerando con una foreach non avrà prestazioni peggiori di altre opzioni simili e pratiche.

Inoltre, vale la pena notare che le persone che implementano i provider LINQ sono incoraggiati a far funzionare i metodi comuni come fanno nei provider forniti da Microsoft, ma non sono obbligati a farlo. Se dovessi scrivere LINQ to HTML o LINQ al mio fornitore di formato dati proprietario non ci sarebbe alcuna garanzia che si comporti in questo modo. Forse la natura dei dati renderebbe l’esecuzione immediata l’unica opzione pratica.

Inoltre, modifica finale; se ti interessa questo C # In Depth di Jon Skeet è molto istruttivo e un’ottima lettura. La mia risposta riassume alcune pagine del libro (si spera con ragionevole accuratezza) ma se vuoi maggiori dettagli su come LINQ funziona sotto le copertine, è un buon posto dove guardare.

prova questo su LinqPad

 void Main() { var testList = Enumerable.Range(1,10); var query = testList.Where(x => { Console.WriteLine(string.Format("Doing where on {0}", x)); return x % 2 == 0; }); Console.WriteLine("First foreach starting"); foreach(var i in query) { Console.WriteLine(string.Format("Foreached where on {0}", i)); } Console.WriteLine("First foreach ending"); Console.WriteLine("Second foreach starting"); foreach(var i in query) { Console.WriteLine(string.Format("Foreached where on {0} for the second time.", i)); } Console.WriteLine("Second foreach ending"); } 

Ogni volta che viene eseguito il delegato, verrà visualizzato un output della console, quindi è ansible visualizzare la query Linq eseguita ogni volta. Ora osservando l’output della console vediamo che il secondo ciclo foreach causa ancora il “Doing where on” per stampare, mostrando quindi che il secondo uso di foreach causa infatti la clausola where di essere eseguita di nuovo … potenzialmente causando un rallentamento .

 First foreach starting Doing where on 1 Doing where on 2 Foreached where on 2 Doing where on 3 Doing where on 4 Foreached where on 4 Doing where on 5 Doing where on 6 Foreached where on 6 Doing where on 7 Doing where on 8 Foreached where on 8 Doing where on 9 Doing where on 10 Foreached where on 10 First foreach ending Second foreach starting Doing where on 1 Doing where on 2 Foreached where on 2 for the second time. Doing where on 3 Doing where on 4 Foreached where on 4 for the second time. Doing where on 5 Doing where on 6 Foreached where on 6 for the second time. Doing where on 7 Doing where on 8 Foreached where on 8 for the second time. Doing where on 9 Doing where on 10 Foreached where on 10 for the second time. Second foreach ending 

Dipende da come viene utilizzata la query di Linq.

 var q = {some linq query here} while (true) { foreach(var item in q) { ... } } 

Il codice sopra eseguirà la query di Linq più volte. Non a causa del foreach, ma perché il foreach si trova all’interno di un altro ciclo, quindi il foreach stesso viene eseguito più volte.

Se tutti i consumatori di una query linq lo usano “attentamente” ed evitano errori stupidi come i cicli nidificati sopra, allora una query linq non dovrebbe essere eseguita più volte inutilmente.

Ci sono occasioni in cui la riduzione di una query linq a un set di risultati in memoria usando ToList () è giustificata, ma secondo me ToList () è usato molto, troppo spesso. ToList () diventa quasi sempre una pillola avvelenata ogni volta che sono coinvolti grandi dati, perché costringe l’intero set di risultati (potenzialmente milioni di righe) a essere trascinato in memoria e memorizzato nella cache, anche se il consumatore / enumeratore più esterno ha bisogno solo di 10 righe. Evita ToList () a meno che tu non abbia una giustificazione molto specifica e sai che i tuoi dati non saranno mai grandi.

A volte potrebbe essere una buona idea “memorizzare” una query LINQ utilizzando ToList() o ToArray() , se si accede più volte alla query nel codice.

Ma tieni a mente che “caching” chiama a turno un foreach .

Quindi la regola base per me è:

  • se una query viene semplicemente utilizzata in un foreach (e questo è), allora non memorizzo la query nella cache
  • se una query viene utilizzata in un foreach e in altri punti del codice, quindi lo memorizzo in una var utilizzando ToList/ToArray

foreach , di per sé, esegue solo una volta i suoi dati. Infatti, lo attraversa in modo specifico una volta. Non puoi guardare avanti o indietro o alterare l’indice nel modo che puoi con un ciclo for .

Tuttavia, se nel codice sono presenti più foreach , che operano tutti sulla stessa query LINQ, è ansible che la query venga eseguita più volte. Questo dipende interamente dai dati , però. Se si sta iterando su un IEnumerable / IQueryable basato su LINQ che rappresenta una query di database, eseguirà tale query ogni volta. Se si sta iterando su un List o su un’altra raccolta di oggetti, verrà eseguito ogni volta nell’elenco, ma non colpirà più volte il database.

In altre parole, questa è una proprietà di LINQ , non una proprietà di foreach .

La differenza è nel tipo sottostante. Poiché LINQ è basato su IEnumerable (o IQueryable), lo stesso operatore LINQ può avere caratteristiche di performance completamente diverse.

Una lista sarà sempre pronta a rispondere, ma richiede uno sforzo iniziale per creare una lista.

Un iteratore è anche IEnumerable e può utilizzare qualsiasi algoritmo ogni volta che recupera l’elemento “successivo”. Questo sarà più veloce se non è effettivamente necessario passare attraverso il set completo di elementi.

È ansible trasformare qualsiasi object IEnumerable in un elenco chiamando ToList () e memorizzando l’elenco risultante in una variabile locale. Questo è consigliabile se

  • Non dipendono dall’esecuzione posticipata.
  • Devi accedere a più elementi totali rispetto all’intero set.
  • Puoi pagare il costo iniziale del recupero e della memorizzazione di tutti gli articoli.

Usando LINQ anche senza quadro ciò che otterrete è che l’esecuzione differita è in vigore. È solo forzando un’iterazione che viene valutata l’espressione linq effettiva. In questo senso, ogni volta che si utilizza l’espressione linq verrà valutata.

Ora con le quadro questo è sempre lo stesso, ma qui c’è solo più funzionalità al lavoro. Quando il framework quadro vede l’espressione per la prima volta, sembra che abbia già eseguito questa query. In caso contrario, andrà al database e recupererà i dati, configurerà il modello di memoria interna e restituirà i dati all’utente. Se il framework delle quadro vede che ha già recuperato i dati in anticipo, non andrà al database e utilizzerà il modello di memoria che ha impostato in precedenza per restituire i dati all’utente.

Questo può semplificarti la vita, ma può anche essere un dolore. Ad esempio se richiedi tutti i record da una tabella usando un’espressione linq. Il framework entity framework caricherà tutti i dati dalla tabella. Se in seguito si valuta la stessa espressione di linq, anche se nel tempo i record sono stati cancellati o aggiunti, si otterrà lo stesso risultato.

La struttura dell’ quadro è una cosa complicata. Ci sono naturalmente dei modi per farlo rieseguire la query, tenendo conto dei cambiamenti che ha nel proprio modello di memoria e simili.

Suggerisco di leggere “framework entity framework” di Julia Lerman. Affronta molti problemi come quello che hai adesso.

Eseguirà la dichiarazione LINQ lo stesso numero di volte, non importa se lo fai. .ToList() o no. Ho un esempio qui con output colorato alla console:

Cosa succede nel codice (vedi codice in fondo):

  • Crea un elenco di 100 pollici (0-99).
  • Creare un’istruzione LINQ che stampa ogni int dall’elenco seguito da due * nella console in colore rosso, quindi restituire l’int se si tratta di un numero pari.
  • Fai una query sulla query , stampando ogni numero pari in verde.
  • Fai una ricerca sulla query.ToList() , stampando ogni numero pari in verde.

Come puoi vedere nell’output sottostante, il numero di interi scritti nella console è lo stesso, nel senso che l’istruzione LINQ viene eseguita lo stesso numero di volte.

La differenza è quando viene eseguita l’istruzione . Come puoi vedere, quando .ToList() un foreach sulla query (che non hai richiamato .ToList() su), l’elenco e l’object IEnumerable, restituito .ToList() LINQ, vengono enumerati contemporaneamente.

Quando si mette in cache per primo l’elenco, vengono enumerati separatamente, ma sempre la stessa quantità di volte.

La differenza è molto importante da comprendere , perché se la lista viene modificata dopo aver definito la tua istruzione LINQ, l’istruzione LINQ funzionerà sulla lista modificata quando viene eseguita (ad es. Da .ToList() ). MA se si forza l’esecuzione .ToList() LINQ ( .ToList() ) e quindi si modifica l’elenco in seguito, l’istruzione LINQ NON funzionerà nell’elenco modificato.

Ecco l’output: Uscita di esecuzione differita LINQ

Ecco il mio codice:

 // Main method: static void Main(string[] args) { IEnumerable ints = Enumerable.Range(0, 100); var query = ints.Where(x => { Console.ForegroundColor = ConsoleColor.Red; Console.Write($"{x}**, "); return x % 2 == 0; }); DoForeach(query, "query"); DoForeach(query, "query.ToList()"); Console.ForegroundColor = ConsoleColor.White; } // DoForeach method: private static void DoForeach(IEnumerable collection, string collectionName) { Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("\n--- {0} FOREACH BEGIN: ---", collectionName); if (collectionName.Contains("query.ToList()")) collection = collection.ToList(); foreach (var item in collection) { Console.ForegroundColor = ConsoleColor.Green; Console.Write($"{item}, "); } Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("\n--- {0} FOREACH END ---", collectionName); } 

Nota sul tempo di esecuzione: ho eseguito alcuni test di temporizzazione (non abbastanza per postarlo qui) e non ho trovato alcuna coerenza in entrambi i metodi essendo più veloce dell’altro (inclusa l’esecuzione di .ToList() nei tempi). Nelle raccolte più grandi, prima di tutto il caching della raccolta e poi l’iterazione sembrava un po ‘più veloce, ma dal mio test non vi erano conclusioni definitive.