Adição após comentário muito útil de mhand no final
Resposta original
Embora a maioria das soluções possa funcionar, acho que não são muito eficientes. Suponha que você queira apenas os primeiros itens dos primeiros pedaços. Então você não gostaria de iterar sobre todos os (zilhões) itens em sua sequência.
O seguinte irá, no máximo, enumerar duas vezes: uma para o Take e outra para o Skip. Não enumerará mais elementos do que você usará:
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
}
}
Quantas vezes isso enumerará a sequência?
Suponha que você divida sua fonte em pedaços de chunkSize
. Você enumera apenas os primeiros N pedaços. De cada pedaço enumerado, você enumerará apenas os primeiros M elementos.
While(source.Any())
{
...
}
o Any receberá o Enumerador, faça 1 MoveNext () e retornará o valor retornado após Disposing the Enumerator. Isso será feito N vezes
yield return source.Take(chunkSize);
De acordo com a fonte de referência, isso fará algo como:
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;
}
}
Isso não faz muito até você começar a enumerar sobre o Chunk buscado. Se você buscar vários Chunks, mas decidir não enumerar o primeiro Chunk, o foreach não será executado, pois seu depurador mostrará a você.
Se você decidir pegar os primeiros M elementos do primeiro pedaço, o retorno do rendimento será executado exatamente M vezes. Isso significa:
- obter o enumerador
- chame MoveNext () e M atual.
- Descarte o enumerador
Depois que o primeiro pedaço tiver sido retornado, pulamos esse primeiro pedaço:
source = source.Skip(chunkSize);
Mais uma vez: vamos dar uma olhada na fonte de referência para encontrar oskipiterator
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;
}
}
}
Como você vê, as SkipIterator
chamadas são feitas MoveNext()
uma vez para cada elemento no Chunk. Não chamaCurrent
.
Portanto, por Chunk, vemos o seguinte:
- Qualquer um (): GetEnumerator; 1 MoveNext (); Dispose Enumerator;
Leva():
- nada se o conteúdo do pedaço não for enumerado.
Se o conteúdo estiver enumerado: GetEnumerator (), um MoveNext e um Current por item enumerado, Dispose enumerator;
Skip (): para cada pedaço enumerado (NÃO o conteúdo do pedaço): GetEnumerator (), MoveNext () chunkSize times, no Current! Dispose enumerator
Se você observar o que acontece com o enumerador, verá muitas chamadas para MoveNext () e apenas chamadas para Current
para os itens do TSource que você realmente decide acessar.
Se você usar N Chunks de tamanho chunkSize, chamará MoveNext ()
- N vezes para Qualquer ()
- ainda não há tempo para o Take, desde que você não enumere os Chunks
- N times chunkSize for Skip ()
Se você decidir enumerar apenas os primeiros M elementos de cada pedaço buscado, precisará chamar MoveNext M vezes por pedaço enumerado.
O total
MoveNext calls: N + N*M + N*chunkSize
Current calls: N*M; (only the items you really access)
Portanto, se você decidir enumerar todos os elementos de todos os pedaços:
MoveNext: numberOfChunks + all elements + all elements = about twice the sequence
Current: every item is accessed exactly once
Se o MoveNext é muito trabalhoso ou não, depende do tipo de sequência de origem. Para listas e matrizes, é um simples incremento de índice, talvez com uma verificação fora do intervalo.
Mas se o seu IEnumerable for o resultado de uma consulta ao banco de dados, verifique se os dados estão realmente materializados no seu computador, caso contrário, os dados serão buscados várias vezes. O DbContext e o Dapper transferirão adequadamente os dados para o processo local antes que possam ser acessados. Se você enumerar a mesma sequência várias vezes, ela não será buscada várias vezes. Dapper retorna um objeto que é uma Lista, o DbContext lembra que os dados já foram buscados.
Depende do seu Repositório se é sensato chamar AsEnumerable () ou ToLists () antes de começar a dividir os itens em Chunks.