Adición después del comentario muy útil de mhand al final
Respuesta original
Aunque la mayoría de las soluciones podrían funcionar, creo que no son muy eficientes. Supongamos que solo desea los primeros elementos de los primeros fragmentos. Entonces no querrás iterar sobre todos los elementos (miles de millones) en tu secuencia.
A continuación, se enumerarán dos veces como máximo: una para Take y otra para Skip. No enumerará más elementos de los que 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
}
}
¿Cuántas veces enumerará esto la secuencia?
Supongamos que divide su fuente en trozos de chunkSize
. Enumera solo los primeros N fragmentos. De cada fragmento enumerado solo enumerará los primeros elementos M.
While(source.Any())
{
...
}
Any obtendrá el enumerador, realiza 1 MoveNext () y devuelve el valor devuelto después de desechar el enumerador. Esto se hará N veces
yield return source.Take(chunkSize);
Según la fuente de referencia, esto hará 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;
}
}
Esto no hace mucho hasta que comience a enumerar sobre el fragmento recuperado. Si obtiene varios Chunks, pero decide no enumerar el primer Chunk, el foreach no se ejecutará, como le mostrará su depurador.
Si decide tomar los primeros M elementos del primer fragmento, el rendimiento se ejecutará exactamente M veces. Esto significa:
- obtener el enumerador
- llame a MoveNext () y Current M veces.
- Deshazte del enumerador
Después de que se haya devuelto el primer fragmento, omitimos este primer fragmento:
source = source.Skip(chunkSize);
Una vez más: echaremos un vistazo a la fuente de referencia para encontrar elskipiterator
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 puede ver, las SkipIterator
llamadas MoveNext()
una vez para cada elemento en el fragmento. No llamaCurrent
.
Entonces, por fragmento, vemos que se hace lo siguiente:
- Cualquiera (): GetEnumerator; 1 MoveNext (); Desechar enumerador;
Tomar():
- nada si el contenido del fragmento no se enumera.
Si se enumera el contenido: GetEnumerator (), un MoveNext y un Current por elemento enumerado, Dispose enumerator;
Saltar (): para cada fragmento que se enumera (NO el contenido del fragmento): GetEnumerator (), MoveNext () chunkSize veces, ¡no actual! Eliminar enumerador
Si observa lo que sucede con el enumerador, verá que hay muchas llamadas a MoveNext (), y solo llamadas a Current
los elementos de TSource a los que realmente decide acceder.
Si toma N trozos de tamaño chunkSize, llama a MoveNext ()
- N veces para Any ()
- todavía no hay tiempo para Take, siempre y cuando no enumeres los Chunks
- N veces fragmento Tamaño para Saltar ()
Si decide enumerar solo los primeros elementos M de cada fragmento recuperado, debe llamar a MoveNext M veces por fragmento enumerado.
El total
MoveNext calls: N + N*M + N*chunkSize
Current calls: N*M; (only the items you really access)
Entonces, si decides enumerar todos los elementos de todos los fragmentos:
MoveNext: numberOfChunks + all elements + all elements = about twice the sequence
Current: every item is accessed exactly once
Si MoveNext es mucho trabajo o no, depende del tipo de secuencia de origen. Para listas y matrices es un simple incremento de índice, con quizás una verificación fuera de rango.
Pero si su IEnumerable es el resultado de una consulta a la base de datos, asegúrese de que los datos realmente se materialicen en su computadora, de lo contrario, los datos se recuperarán varias veces. DbContext y Dapper transferirán correctamente los datos al proceso local antes de poder acceder a ellos. Si enumera la misma secuencia varias veces, no se obtiene varias veces. Dapper devuelve un objeto que es una Lista, DbContext recuerda que los datos ya están recuperados.
Depende de su repositorio si es aconsejable llamar a AsEnumerable () o ToLists () antes de comenzar a dividir los elementos en fragmentos