Réponses:
Vous n'avez pas besoin d'écrire de code. Utilisez la méthode MoreLINQ Batch, qui regroupe la séquence source dans des compartiments dimensionnés (MoreLINQ est disponible en tant que package NuGet que vous pouvez installer):
int size = 10;
var batches = sequence.Batch(size);
Qui est implémenté comme:
public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
this IEnumerable<TSource> source, int size)
{
TSource[] bucket = null;
var count = 0;
foreach (var item in source)
{
if (bucket == null)
bucket = new TSource[size];
bucket[count++] = item;
if (count != size)
continue;
yield return bucket;
bucket = null;
count = 0;
}
if (bucket != null && count > 0)
yield return bucket.Take(count).ToArray();
}
Batch(new int[] { 1, 2 }, 1000000)
public static class MyExtensions
{
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> items,
int maxItems)
{
return items.Select((item, inx) => new { item, inx })
.GroupBy(x => x.inx / maxItems)
.Select(g => g.Select(x => x.item));
}
}
et l'utilisation serait:
List<int> list = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
foreach(var batch in list.Batch(3))
{
Console.WriteLine(String.Join(",",batch));
}
PRODUCTION:
0,1,2
3,4,5
6,7,8
9
GroupBy
énumération commencée, n'a-t-il pas besoin d'énumérer complètement sa source? Cela perd une évaluation paresseuse de la source et donc, dans certains cas, tous les avantages du batching!
Si vous commencez par sequence
défini comme an IEnumerable<T>
, et que vous savez qu'il peut être énuméré plusieurs fois en toute sécurité (par exemple parce qu'il s'agit d'un tableau ou d'une liste), vous pouvez simplement utiliser ce modèle simple pour traiter les éléments par lots:
while (sequence.Any())
{
var batch = sequence.Take(10);
sequence = sequence.Skip(10);
// do whatever you need to do with each batch here
}
Tous les éléments ci-dessus fonctionnent terriblement avec de gros lots ou un espace mémoire faible. J'ai dû écrire le mien qui sera pipeline (ne remarquez aucune accumulation d'objets nulle part):
public static class BatchLinq {
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size) {
if (size <= 0)
throw new ArgumentOutOfRangeException("size", "Must be greater than zero.");
using (IEnumerator<T> enumerator = source.GetEnumerator())
while (enumerator.MoveNext())
yield return TakeIEnumerator(enumerator, size);
}
private static IEnumerable<T> TakeIEnumerator<T>(IEnumerator<T> source, int size) {
int i = 0;
do
yield return source.Current;
while (++i < size && source.MoveNext());
}
}
Edit: Le problème connu avec cette approche est que chaque lot doit être énuméré et entièrement énuméré avant de passer au lot suivant. Par exemple, cela ne fonctionne pas:
//Select first item of every 100 items
Batch(list, 100).Select(b => b.First())
Il s'agit d'une implémentation à une fonction de Batch, totalement paresseuse, à faible surcharge, qui ne fait aucune accumulation. Basé sur (et corrige les problèmes dans) la solution de Nick Whaley avec l'aide d'EricRoller.
L'itération provient directement du IEnumerable sous-jacent, de sorte que les éléments doivent être énumérés dans un ordre strict et accessibles pas plus d'une fois. Si certains éléments ne sont pas consommés dans une boucle interne, ils sont rejetés (et essayer d'y accéder à nouveau via un itérateur enregistré sera lancé InvalidOperationException: Enumeration already finished.
).
Vous pouvez tester un échantillon complet sur .NET Fiddle .
public static class BatchLinq
{
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
{
if (size <= 0)
throw new ArgumentOutOfRangeException("size", "Must be greater than zero.");
using (var enumerator = source.GetEnumerator())
while (enumerator.MoveNext())
{
int i = 0;
// Batch is a local function closing over `i` and `enumerator` that
// executes the inner batch enumeration
IEnumerable<T> Batch()
{
do yield return enumerator.Current;
while (++i < size && enumerator.MoveNext());
}
yield return Batch();
while (++i < size && enumerator.MoveNext()); // discard skipped items
}
}
}
done
en appelant toujours e.Count()
après yield return e
. Vous devrez réorganiser la boucle dans BatchInner pour ne pas appeler le comportement non défini source.Current
si i >= size
. Cela éliminera le besoin d'allouer un nouveau BatchInner
pour chaque lot.
i
donc ce n'est pas nécessairement plus efficace que de définir une classe séparée, mais c'est un peu plus propre, je pense.
Je me demande pourquoi personne n'a jamais publié de solution à l'ancienne. En voici une:
List<int> source = Enumerable.Range(1,23).ToList();
int batchsize = 10;
for (int i = 0; i < source.Count; i+= batchsize)
{
var batch = source.Skip(i).Take(batchsize);
}
Cette simplicité est possible car la méthode Take:
... énumère
source
et produit des éléments jusqu'à ce que descount
éléments aient été générés ousource
ne contienne plus d'éléments. Sicount
dépasse le nombre d'éléments danssource
, tous les éléments desource
sont renvoyés
Avertissement:
L'utilisation de Skip et Take à l'intérieur de la boucle signifie que l'énumérable sera énuméré plusieurs fois. Ceci est dangereux si l'énumérable est différé. Cela peut entraîner plusieurs exécutions d'une requête de base de données, d'une requête Web ou d'une lecture de fichier. Cet exemple est explicitement pour l'utilisation d'une liste qui n'est pas différée, donc c'est moins un problème. C'est toujours une solution lente puisque skip énumérera la collection à chaque fois qu'elle est appelée.
Cela peut également être résolu en utilisant la GetRange
méthode, mais cela nécessite un calcul supplémentaire pour extraire un éventuel lot de repos:
for (int i = 0; i < source.Count; i += batchsize)
{
int remaining = source.Count - i;
var batch = remaining > batchsize ? source.GetRange(i, batchsize) : source.GetRange(i, remaining);
}
Voici une troisième façon de gérer cela, qui fonctionne avec 2 boucles. Cela garantit que la collection n'est énumérée qu'une seule fois!:
int batchsize = 10;
List<int> batch = new List<int>(batchsize);
for (int i = 0; i < source.Count; i += batchsize)
{
// calculated the remaining items to avoid an OutOfRangeException
batchsize = source.Count - i > batchsize ? batchsize : source.Count - i;
for (int j = i; j < i + batchsize; j++)
{
batch.Add(source[j]);
}
batch.Clear();
}
Skip
et Take
à l'intérieur de la boucle signifie que l'énumérable sera énuméré plusieurs fois. Ceci est dangereux si l'énumérable est différé. Cela peut entraîner plusieurs exécutions d'une requête de base de données, d'une requête Web ou d'une lecture de fichier. Dans votre exemple, vous avez un List
qui n'est pas différé, donc c'est moins un problème.
Même approche que MoreLINQ, mais en utilisant List au lieu de Array. Je n'ai pas fait de benchmarking, mais la lisibilité est plus importante pour certaines personnes:
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
{
List<T> batch = new List<T>();
foreach (var item in source)
{
batch.Add(item);
if (batch.Count >= size)
{
yield return batch;
batch.Clear();
}
}
if (batch.Count > 0)
{
yield return batch;
}
}
size
paramètre à votre new List
pour optimiser sa taille.
batch.Clear();
batch = new List<T>();
Voici une tentative d'amélioration des implémentations paresseuses de Nick Whaley ( lien ) et d'infogulch ( lien ) Batch
. Celui-ci est strict. Soit vous énumérez les lots dans le bon ordre, soit vous obtenez une exception.
public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
this IEnumerable<TSource> source, int size)
{
if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
using (var enumerator = source.GetEnumerator())
{
int i = 0;
while (enumerator.MoveNext())
{
if (i % size != 0) throw new InvalidOperationException(
"The enumeration is out of order.");
i++;
yield return GetBatch();
}
IEnumerable<TSource> GetBatch()
{
while (true)
{
yield return enumerator.Current;
if (i % size == 0 || !enumerator.MoveNext()) break;
i++;
}
}
}
}
Et voici une Batch
implémentation paresseuse pour les sources de type IList<T>
. Celui-ci n'impose aucune restriction sur le dénombrement. Les lots peuvent être énumérés partiellement, dans n'importe quel ordre et plusieurs fois. La restriction de ne pas modifier la collection pendant l'énumération est cependant toujours en place. Ceci est réalisé en effectuant un appel factice enumerator.MoveNext()
avant de donner un morceau ou un élément. L'inconvénient est que l'énumérateur n'est pas éliminé, car on ne sait pas quand l'énumération va se terminer.
public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
this IList<TSource> source, int size)
{
if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
var enumerator = source.GetEnumerator();
for (int i = 0; i < source.Count; i += size)
{
enumerator.MoveNext();
yield return GetChunk(i, Math.Min(i + size, source.Count));
}
IEnumerable<TSource> GetChunk(int from, int toExclusive)
{
for (int j = from; j < toExclusive; j++)
{
enumerator.MoveNext();
yield return source[j];
}
}
}
Je rejoins ça très tard mais j'ai trouvé quelque chose de plus intéressant.
Nous pouvons donc l'utiliser ici Skip
et Take
pour de meilleures performances.
public static class MyExtensions
{
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> items, int maxItems)
{
return items.Select((item, index) => new { item, index })
.GroupBy(x => x.index / maxItems)
.Select(g => g.Select(x => x.item));
}
public static IEnumerable<T> Batch2<T>(this IEnumerable<T> items, int skip, int take)
{
return items.Skip(skip).Take(take);
}
}
Ensuite, j'ai vérifié avec 100000 enregistrements. La boucle seule prend plus de temps en cas deBatch
Code de l'application console.
static void Main(string[] args)
{
List<string> Ids = GetData("First");
List<string> Ids2 = GetData("tsriF");
Stopwatch FirstWatch = new Stopwatch();
FirstWatch.Start();
foreach (var batch in Ids2.Batch(5000))
{
// Console.WriteLine("Batch Ouput:= " + string.Join(",", batch));
}
FirstWatch.Stop();
Console.WriteLine("Done Processing time taken:= "+ FirstWatch.Elapsed.ToString());
Stopwatch Second = new Stopwatch();
Second.Start();
int Length = Ids2.Count;
int StartIndex = 0;
int BatchSize = 5000;
while (Length > 0)
{
var SecBatch = Ids2.Batch2(StartIndex, BatchSize);
// Console.WriteLine("Second Batch Ouput:= " + string.Join(",", SecBatch));
Length = Length - BatchSize;
StartIndex += BatchSize;
}
Second.Stop();
Console.WriteLine("Done Processing time taken Second:= " + Second.Elapsed.ToString());
Console.ReadKey();
}
static List<string> GetData(string name)
{
List<string> Data = new List<string>();
for (int i = 0; i < 100000; i++)
{
Data.Add(string.Format("{0} {1}", name, i.ToString()));
}
return Data;
}
Le temps pris est comme ça.
Première - 00: 00: 00.0708, 00: 00: 00.0660
Deuxième (Take and Skip One) - 00: 00: 00.0008, 00: 00: 00.0008
GroupBy
énumère complètement avant de produire une seule ligne. Ce n'est pas une bonne façon de procéder au traitement par lots.
foreach (var batch in Ids2.Batch(5000))
à var gourpBatch = Ids2.Batch(5000)
et vérifiez les résultats chronométrés. ou ajouter tolist à var SecBatch = Ids2.Batch2(StartIndex, BatchSize);
je serais intéressé si vos résultats pour le changement de timing.
Donc, avec un chapeau fonctionnel, cela semble trivial ... mais en C #, il y a des inconvénients importants.
vous verriez probablement cela comme un déroulement de IEnumerable (google et vous vous retrouverez probablement dans certains documents Haskell, mais il peut y avoir des trucs F # utilisant déplier, si vous connaissez F #, louchez sur les documents Haskell et cela fera sens).
Déplier est lié au repli ("agrégat") sauf qu'au lieu d'itérer via l'entrée IEnumerable, il itère à travers les structures de données de sortie (c'est une relation similaire entre IEnumerable et IObservable, en fait, je pense que IObservable implémente un "déplier" appelé générer. ..)
de toute façon vous avez d'abord besoin d'une méthode de dépliage, je pense que cela fonctionne (malheureusement, cela finira par faire exploser la pile pour les grandes "listes" ... vous pouvez l'écrire en toute sécurité en F # en utilisant yield! plutôt que concat);
static IEnumerable<T> Unfold<T, U>(Func<U, IEnumerable<Tuple<U, T>>> f, U seed)
{
var maybeNewSeedAndElement = f(seed);
return maybeNewSeedAndElement.SelectMany(x => new[] { x.Item2 }.Concat(Unfold(f, x.Item1)));
}
c'est un peu obtus parce que C # n'implémente pas certaines des choses que les langages fonctionnels tiennent pour acquises ... mais il prend essentiellement une graine et génère ensuite une réponse "Peut-être" de l'élément suivant dans IEnumerable et de la graine suivante (Peut-être n'existe pas en C #, donc nous avons utilisé IEnumerable pour le simuler), et concatène le reste de la réponse (je ne peux pas garantir la complexité "O (n?)" de ceci).
Une fois que vous avez fait cela, alors;
static IEnumerable<IEnumerable<T>> Batch<T>(IEnumerable<T> xs, int n)
{
return Unfold(ys =>
{
var head = ys.Take(n);
var tail = ys.Skip(n);
return head.Take(1).Select(_ => Tuple.Create(tail, head));
},
xs);
}
tout semble assez propre ... vous prenez les éléments "n" comme élément "suivant" dans IEnumerable, et la "queue" est le reste de la liste non traitée.
s'il n'y a rien dans la tête ... vous avez terminé ... vous retournez "Nothing" (mais truqué comme un IEnumerable vide>) ... sinon vous retournez l'élément head et la queue à traiter.
vous pouvez probablement le faire en utilisant IObservable, il y a probablement déjà une méthode de type "Batch", et vous pouvez probablement l'utiliser.
Si le risque de débordement de pile vous inquiète (c'est probablement le cas), alors vous devriez l'implémenter en F # (et il y a probablement déjà une bibliothèque F # (FSharpX?) Avec ça).
(Je n'ai fait que quelques tests rudimentaires à ce sujet, il peut donc y avoir des bugs étranges).
J'ai écrit une implémentation IEnumerable personnalisée qui fonctionne sans linq et garantit une seule énumération sur les données. Il accomplit également tout cela sans nécessiter de listes de sauvegarde ou de tableaux qui provoquent des explosions de mémoire sur de grands ensembles de données.
Voici quelques tests de base:
[Fact]
public void ShouldPartition()
{
var ints = new List<int> {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
var data = ints.PartitionByMaxGroupSize(3);
data.Count().Should().Be(4);
data.Skip(0).First().Count().Should().Be(3);
data.Skip(0).First().ToList()[0].Should().Be(0);
data.Skip(0).First().ToList()[1].Should().Be(1);
data.Skip(0).First().ToList()[2].Should().Be(2);
data.Skip(1).First().Count().Should().Be(3);
data.Skip(1).First().ToList()[0].Should().Be(3);
data.Skip(1).First().ToList()[1].Should().Be(4);
data.Skip(1).First().ToList()[2].Should().Be(5);
data.Skip(2).First().Count().Should().Be(3);
data.Skip(2).First().ToList()[0].Should().Be(6);
data.Skip(2).First().ToList()[1].Should().Be(7);
data.Skip(2).First().ToList()[2].Should().Be(8);
data.Skip(3).First().Count().Should().Be(1);
data.Skip(3).First().ToList()[0].Should().Be(9);
}
La méthode d'extension pour partitionner les données.
/// <summary>
/// A set of extension methods for <see cref="IEnumerable{T}"/>.
/// </summary>
public static class EnumerableExtender
{
/// <summary>
/// Splits an enumerable into chucks, by a maximum group size.
/// </summary>
/// <param name="source">The source to split</param>
/// <param name="maxSize">The maximum number of items per group.</param>
/// <typeparam name="T">The type of item to split</typeparam>
/// <returns>A list of lists of the original items.</returns>
public static IEnumerable<IEnumerable<T>> PartitionByMaxGroupSize<T>(this IEnumerable<T> source, int maxSize)
{
return new SplittingEnumerable<T>(source, maxSize);
}
}
C'est la classe d'implémentation
using System.Collections;
using System.Collections.Generic;
internal class SplittingEnumerable<T> : IEnumerable<IEnumerable<T>>
{
private readonly IEnumerable<T> backing;
private readonly int maxSize;
private bool hasCurrent;
private T lastItem;
public SplittingEnumerable(IEnumerable<T> backing, int maxSize)
{
this.backing = backing;
this.maxSize = maxSize;
}
public IEnumerator<IEnumerable<T>> GetEnumerator()
{
return new Enumerator(this, this.backing.GetEnumerator());
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
private class Enumerator : IEnumerator<IEnumerable<T>>
{
private readonly SplittingEnumerable<T> parent;
private readonly IEnumerator<T> backingEnumerator;
private NextEnumerable current;
public Enumerator(SplittingEnumerable<T> parent, IEnumerator<T> backingEnumerator)
{
this.parent = parent;
this.backingEnumerator = backingEnumerator;
this.parent.hasCurrent = this.backingEnumerator.MoveNext();
if (this.parent.hasCurrent)
{
this.parent.lastItem = this.backingEnumerator.Current;
}
}
public bool MoveNext()
{
if (this.current == null)
{
this.current = new NextEnumerable(this.parent, this.backingEnumerator);
return true;
}
else
{
if (!this.current.IsComplete)
{
using (var enumerator = this.current.GetEnumerator())
{
while (enumerator.MoveNext())
{
}
}
}
}
if (!this.parent.hasCurrent)
{
return false;
}
this.current = new NextEnumerable(this.parent, this.backingEnumerator);
return true;
}
public void Reset()
{
throw new System.NotImplementedException();
}
public IEnumerable<T> Current
{
get { return this.current; }
}
object IEnumerator.Current
{
get { return this.Current; }
}
public void Dispose()
{
}
}
private class NextEnumerable : IEnumerable<T>
{
private readonly SplittingEnumerable<T> splitter;
private readonly IEnumerator<T> backingEnumerator;
private int currentSize;
public NextEnumerable(SplittingEnumerable<T> splitter, IEnumerator<T> backingEnumerator)
{
this.splitter = splitter;
this.backingEnumerator = backingEnumerator;
}
public bool IsComplete { get; private set; }
public IEnumerator<T> GetEnumerator()
{
return new NextEnumerator(this.splitter, this, this.backingEnumerator);
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
private class NextEnumerator : IEnumerator<T>
{
private readonly SplittingEnumerable<T> splitter;
private readonly NextEnumerable parent;
private readonly IEnumerator<T> enumerator;
private T currentItem;
public NextEnumerator(SplittingEnumerable<T> splitter, NextEnumerable parent, IEnumerator<T> enumerator)
{
this.splitter = splitter;
this.parent = parent;
this.enumerator = enumerator;
}
public bool MoveNext()
{
this.parent.currentSize += 1;
this.currentItem = this.splitter.lastItem;
var hasCcurent = this.splitter.hasCurrent;
this.parent.IsComplete = this.parent.currentSize > this.splitter.maxSize;
if (this.parent.IsComplete)
{
return false;
}
if (hasCcurent)
{
var result = this.enumerator.MoveNext();
this.splitter.lastItem = this.enumerator.Current;
this.splitter.hasCurrent = result;
}
return hasCcurent;
}
public void Reset()
{
throw new System.NotImplementedException();
}
public T Current
{
get { return this.currentItem; }
}
object IEnumerator.Current
{
get { return this.Current; }
}
public void Dispose()
{
}
}
}
}
Je sais que tout le monde a utilisé des systèmes complexes pour faire ce travail, et je ne comprends vraiment pas pourquoi. Take and skip autorisera toutes ces opérations en utilisant la Func<TSource,Int32,TResult>
fonction de sélection commune avec transformation. Comme:
public IEnumerable<IEnumerable<T>> Buffer<T>(IEnumerable<T> source, int size)=>
source.Select((item, index) => source.Skip(size * index).Take(size)).TakeWhile(bucket => bucket.Any());
source
sera répété très souvent.
Enumerable.Range(0, 1).SelectMany(_ => Enumerable.Range(0, new Random().Next()))
.
Juste une autre implémentation d'une ligne. Cela fonctionne même avec une liste vide, dans ce cas, vous obtenez une collection de lots de taille nulle.
var aList = Enumerable.Range(1, 100).ToList(); //a given list
var size = 9; //the wanted batch size
//number of batches are: (aList.Count() + size - 1) / size;
var batches = Enumerable.Range(0, (aList.Count() + size - 1) / size).Select(i => aList.GetRange( i * size, Math.Min(size, aList.Count() - i * size)));
Assert.True(batches.Count() == 12);
Assert.AreEqual(batches.ToList().ElementAt(0), new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
Assert.AreEqual(batches.ToList().ElementAt(1), new List<int>() { 10, 11, 12, 13, 14, 15, 16, 17, 18 });
Assert.AreEqual(batches.ToList().ElementAt(11), new List<int>() { 100 });
Une autre façon consiste à utiliser l' opérateur Rx Buffer
//using System.Linq;
//using System.Reactive.Linq;
//using System.Reactive.Threading.Tasks;
var observableBatches = anAnumerable.ToObservable().Buffer(size);
var batches = aList.ToObservable().Buffer(size).ToList().ToTask().GetAwaiter().GetResult();
GetAwaiter().GetResult()
. Il s'agit d'une odeur de code pour le code synchrone appelant avec force du code asynchrone.
static IEnumerable<IEnumerable<T>> TakeBatch<T>(IEnumerable<T> ts,int batchSize)
{
return from @group in ts.Select((x, i) => new { x, i }).ToLookup(xi => xi.i / batchSize)
select @group.Select(xi => xi.x);
}