Ajout après un commentaire très utile de mhand à la fin
Réponse originale
Bien que la plupart des solutions puissent fonctionner, je pense qu'elles ne sont pas très efficaces. Supposons que vous ne souhaitiez que les premiers éléments des premiers morceaux. Ensuite, vous ne voudriez pas parcourir tous les éléments (zillion) de votre séquence.
Les éléments suivants seront au maximum énumérés deux fois: une fois pour la prise et une fois pour le saut. Il n'énumérera pas plus d'éléments que vous n'en utiliserez:
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
}
}
Combien de fois cela énumérera-t-il la séquence?
Supposons que vous divisiez votre source en morceaux de chunkSize
. Vous énumérez uniquement les N premiers morceaux. À partir de chaque bloc énuméré, vous n'énumérerez que les premiers éléments M.
While(source.Any())
{
...
}
Any obtiendra l'énumérateur, effectuez 1 MoveNext () et retourne la valeur retournée après avoir supprimé l'énumérateur. Cela se fera N fois
yield return source.Take(chunkSize);
Selon la source de référence, cela fera quelque chose comme:
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;
}
}
Cela ne fait pas grand-chose jusqu'à ce que vous commenciez à énumérer le morceau récupéré. Si vous récupérez plusieurs morceaux, mais décidez de ne pas énumérer le premier morceau, foreach n'est pas exécuté, comme votre débogueur vous le montrera.
Si vous décidez de prendre les premiers M éléments du premier bloc, le rendement est exécuté exactement M fois. Ça signifie:
- obtenir l'énumérateur
- appelez MoveNext () et les heures M actuelles.
- Jeter l'énumérateur
Après le retour du premier morceau, nous sautons ce premier morceau:
source = source.Skip(chunkSize);
Encore une fois: nous allons jeter un oeil à la source de référence pour trouver leskipiterator
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;
}
}
}
Comme vous le voyez, les SkipIterator
appels MoveNext()
une fois pour chaque élément du bloc. Ça n'appelle pas Current
.
Donc, par morceau, nous voyons que ce qui suit est fait:
- Any (): GetEnumerator; 1 MoveNext (); Dispose Enumerator;
Prendre():
- rien si le contenu du morceau n'est pas énuméré.
Si le contenu est énuméré: GetEnumerator (), un MoveNext et un Current par élément énuméré, Dispose enumerator;
Skip (): pour chaque bloc qui est énuméré (PAS le contenu du bloc): GetEnumerator (), MoveNext () fois chunkSize, pas de courant! Éliminer l'énumérateur
Si vous regardez ce qui se passe avec l'énumérateur, vous verrez qu'il y a beaucoup d'appels à MoveNext (), et uniquement des appels à Current
pour les éléments TSource auxquels vous décidez réellement d'accéder.
Si vous prenez N morceaux de taille chunkSize, alors les appels à MoveNext ()
- N fois pour Any ()
- pas encore de temps pour Take, tant que vous n'énumérez pas les morceaux
- N fois chunkSize pour Skip ()
Si vous décidez d'énumérer uniquement les premiers éléments M de chaque bloc récupéré, vous devez appeler MoveNext M fois par bloc énuméré.
Le total
MoveNext calls: N + N*M + N*chunkSize
Current calls: N*M; (only the items you really access)
Donc, si vous décidez d'énumérer tous les éléments de tous les morceaux:
MoveNext: numberOfChunks + all elements + all elements = about twice the sequence
Current: every item is accessed exactly once
Que MoveNext représente beaucoup de travail ou non, cela dépend du type de séquence source. Pour les listes et les tableaux, il s'agit d'un simple incrément d'index, avec peut-être une vérification hors plage.
Mais si votre IEnumerable est le résultat d'une requête de base de données, assurez-vous que les données sont réellement matérialisées sur votre ordinateur, sinon les données seront récupérées plusieurs fois. DbContext et Dapper transfèreront correctement les données au processus local avant de pouvoir y accéder. Si vous énumérez plusieurs fois la même séquence, elle n'est pas récupérée plusieurs fois. Dapper renvoie un objet qui est une liste, DbContext se souvient que les données sont déjà récupérées.
Cela dépend de votre référentiel s'il est sage d'appeler AsEnumerable () ou ToLists () avant de commencer à diviser les éléments en morceaux