Aggiunta dopo un commento molto utile di mhand alla fine
Risposta originale
Sebbene la maggior parte delle soluzioni potrebbe funzionare, penso che non siano molto efficienti. Supponiamo che tu voglia solo i primi elementi dei primi pezzi. Quindi non vorrai iterare su tutti (zillion) elementi nella sequenza.
Quanto segue verrà elencato al massimo due volte: una volta per il Take e una volta per il Skip. Non verrà elencato su più elementi di quanti ne utilizzerai:
public static IEnumerable<IEnumerable<TSource>> ChunkBy<TSource>
(this IEnumerable<TSource> source, int chunkSize)
{
while (source.Any()) // while there are elements left
{ // still something to chunk:
yield return source.Take(chunkSize); // return a chunk of chunkSize
source = source.Skip(chunkSize); // skip the returned chunk
}
}
Quante volte enumererà la sequenza?
Supponiamo di dividere la fonte in pezzi di chunkSize. Enumera solo i primi N pezzi. Da ogni blocco elencato enumererai solo i primi elementi M.
While(source.Any())
{
...
}
Any otterrà l'Enumeratore, eseguirà 1 MoveNext () e restituirà il valore restituito dopo aver eliminato l'Enumeratore. Questo sarà fatto N volte
yield return source.Take(chunkSize);
Secondo la fonte di riferimento questo farà qualcosa del tipo:
public static IEnumerable<TSource> Take<TSource>(this IEnumerable<TSource> source, int count)
{
return TakeIterator<TSource>(source, count);
}
static IEnumerable<TSource> TakeIterator<TSource>(IEnumerable<TSource> source, int count)
{
foreach (TSource element in source)
{
yield return element;
if (--count == 0) break;
}
}
Questo non fa molto finché non si inizia a enumerare il Chunk recuperato. Se recuperi più blocchi, ma decidi di non enumerare il primo blocco, il foreach non viene eseguito, come verrà mostrato dal debugger.
Se decidi di prendere i primi elementi M del primo blocco, il rendimento viene eseguito esattamente M volte. Questo significa:
- ottenere l'enumeratore
- chiama i tempi MoveNext () e Current M.
- Smaltire l'enumeratore
Dopo che il primo blocco è stato restituito, saltiamo questo primo pezzo:
source = source.Skip(chunkSize);
Ancora una volta: daremo un'occhiata alla fonte di riferimento per trovare ilskipiterator
static IEnumerable<TSource> SkipIterator<TSource>(IEnumerable<TSource> source, int count)
{
using (IEnumerator<TSource> e = source.GetEnumerator())
{
while (count > 0 && e.MoveNext()) count--;
if (count <= 0)
{
while (e.MoveNext()) yield return e.Current;
}
}
}
Come vedi, SkipIteratorchiama MoveNext()una volta per ogni elemento nel Chunk. Non chiama Current.
Quindi per Chunk vediamo che è stato fatto quanto segue:
- Any (): GetEnumerator; 1 MoveNext (); Enumeratore di smaltimento;
Prendere():
- nulla se il contenuto del blocco non è elencato.
Se il contenuto è elencato: GetEnumerator (), uno MoveNext e uno Current per elemento elencato, Dispose enumerator;
Skip (): per ogni blocco elencato (NON il contenuto del blocco): GetEnumerator (), MoveNext () volte chunkSize, nessuna corrente! Smaltire l'enumeratore
Se osservi cosa succede con l'enumeratore, noterai che ci sono molte chiamate a MoveNext () e chiamate solo agli Currentelementi TSource a cui decidi effettivamente accedere.
Se prendi N blocchi di dimensioni chunkSize, quindi chiama a MoveNext ()
- N volte per Any ()
- non c'è ancora tempo per Take, purché non si enumerino i blocchi
- N times chunkSize for Skip ()
Se decidi di enumerare solo i primi elementi M di ogni blocco recuperato, devi chiamare MoveNext M volte per blocco elencato.
Il totale
MoveNext calls: N + N*M + N*chunkSize
Current calls: N*M; (only the items you really access)
Quindi se decidi di enumerare tutti gli elementi di tutti i blocchi:
MoveNext: numberOfChunks + all elements + all elements = about twice the sequence
Current: every item is accessed exactly once
Se MoveNext richiede molto lavoro o meno, dipende dal tipo di sequenza di origine. Per elenchi e matrici è un semplice incremento dell'indice, con forse un controllo fuori portata.
Ma se IEnumerable è il risultato di una query del database, assicurati che i dati siano materializzati sul tuo computer, altrimenti verranno recuperati più volte. DbContext e Dapper trasferiranno correttamente i dati al processo locale prima di accedervi. Se si enumera la stessa sequenza più volte, questa non viene recuperata più volte. Dapper restituisce un oggetto che è un elenco, DbContext ricorda che i dati sono già stati recuperati.
Dipende dal tuo repository se è saggio chiamare AsEnumerable () o ToLists () prima di iniziare a dividere gli elementi in blocchi