Créer des lots dans linq

104

Quelqu'un peut-il suggérer un moyen de créer des lots d'une certaine taille dans linq?

Dans l'idéal, je veux pouvoir effectuer des opérations par blocs d'une certaine quantité configurable.

BlakeH
la source

Réponses:

116

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();
}
Sergey Berezovskiy
la source
3
4 octets par élément fonctionnent terriblement ? Avez-vous des tests qui montrent ce que signifie terriblement ? Si vous chargez des millions d'éléments en mémoire, je ne le ferais pas. Utiliser la pagination côté serveur
Sergey Berezovskiy
4
Je ne veux pas vous offenser, mais il existe des solutions plus simples qui ne s'accumulent pas du tout. De plus, cela allouera de l'espace même pour des éléments inexistants:Batch(new int[] { 1, 2 }, 1000000)
Nick Whaley
7
@NickWhaley eh bien, d'accord avec vous qu'un espace supplémentaire sera alloué, mais dans la vraie vie, vous avez généralement une situation juste opposée - liste de 1000 articles qui devraient aller par lots de 50 :)
Sergey Berezovskiy
1
Oui, la situation devrait généralement être l'inverse, mais dans la vraie vie, il peut s'agir d'entrées d'utilisateurs.
Nick Whaley
8
C'est une solution parfaitement fine. Dans la vraie vie, vous: validez les entrées de l'utilisateur, traitez les lots comme des collections entières d'éléments (qui accumule les éléments de toute façon) et traitez souvent les lots en parallèle (ce qui n'est pas pris en charge par l'approche itératrice, et sera une mauvaise surprise à moins que vous ne connaissiez le Détails d'implémentation).
Michael Petito
90
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
KG
la source
A travaillé parfait pour moi
FunMatters
16
Une fois l' 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!
ErikE
1
Wow, merci, tu m'as sauvé de la folie. Fonctionne très bien
Riaan de Lange
3
Comme le mentionne @ErikE, cette méthode énumère complètement sa source, donc même si elle a l'air sympa, elle va à l'
encontre
1
Faites ceci - c'est tout à fait approprié lorsque vous devez diviser un bloc de choses existant en plus petits lots de choses pour un traitement performant. L'alternative est une boucle de recherche grossière où vous divisez manuellement les lots et continuez à parcourir toute la source.
StingyJack
31

Si vous commencez par sequencedé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
}
Matthew Strawbridge
la source
2
Moyen
5
@DevHawk: ça l'est. Notez, cependant, que les performances souffriront de façon exponentielle sur les grandes collections (r).
RobIII
28

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())
Nick Whaley
la source
1
La routine @LB publiée ci-dessus n'effectue pas non plus l'accumulation d'éléments.
neontapir
2
@neontapir le fait toujours. Une machine de tri de pièces de monnaie qui vous donne d'abord des nickels, puis des dimes, DOIT d'abord inspecter chaque pièce avant de vous donner un centime pour être sûr qu'il n'y a plus de nickels.
Nick Whaley
2
Ahhh ahha, j'ai manqué votre note d'édition quand j'ai accroché ce code. Il a fallu un certain temps pour comprendre pourquoi l'itération sur des lots non énumérés a en fait énuméré toute la collection d'origine (!!!), fournissant X lots, chacun ayant énuméré 1 élément (où X est le nombre d'éléments de la collection d'origine).
eli
2
@NickWhaley si je fais Count () sur le résultat IEnumerable <IEnumerable <T>> par votre code, cela donne une mauvaise réponse, cela donne le nombre total d'éléments, lorsque prévu est le nombre total de lots créés. Ce n'est pas le cas avec MoreLinq Batch code
Mrinal Kamboj
1
@JohnZabroski - Voici un bref résumé: gist.github.com/mmurrell/9225ed7c4d107c2195057f77e07f0f68
Matt Murrell
24

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
            }
    }
}
infogulch
la source
2
C'est la seule implémentation totalement paresseuse ici. Conforme à l'implémentation python itertools.GroupBy.
Eric Roller
1
Vous pouvez éliminer le chèque doneen 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.Currentsi i >= size. Cela éliminera le besoin d'allouer un nouveau BatchInnerpour chaque lot.
Eric Roller
1
Vous avez raison, vous devez toujours capturer des informations sur la progression de chaque lot. J'ai trouvé un bogue dans votre code si vous essayez d'obtenir le deuxième élément de chaque lot: bug fiddle . L'implémentation fixe sans classe séparée (utilisant C # 7) est ici: fiddle fixe . Notez que je m'attends à ce que le CLR crée toujours la fonction locale une fois par boucle pour capturer la variable, idonc 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.
Eric Roller
1
J'ai comparé cette version à l'aide de BenchmarkDotNet à System.Reactive.Linq.EnumerableEx.Buffer et votre implémentation était 3-4 plus rapide, au risque de la sécurité. En interne, EnumerableEx.Buffer alloue une file d'attente de List <T> github.com/dotnet/reactive/blob/…
John Zabroski
1
Si vous voulez une version tamponnée de ceci, vous pouvez faire: public static IEnumerable <IReadOnlyList <T>> BatchBuffered <T> (this IEnumerable <T> source, int size) => Batch (source, size) .Select (chunk = > (IReadOnlyList <T>) chunk.ToList ()); L'utilisation de IReadOnlyList <T> consiste à indiquer à l'utilisateur que la sortie est mise en cache. Vous pouvez également conserver IEnumerable <IEnumerable <T>> à la place.
gfache
12

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 sourceet produit des éléments jusqu'à ce que des countéléments aient été générés ou sourcene contienne plus d'éléments. Si countdépasse le nombre d'éléments dans source, tous les éléments de sourcesont 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 GetRangemé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();
}
Mong Zhu
la source
2
Très belle solution. Les gens ont oublié comment utiliser la boucle for
VitalickS
1
L'utilisation Skipet 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 Listqui n'est pas différé, donc c'est moins un problème.
Theodor Zoulias
@TheodorZoulias oui je sais, c'est en fait pourquoi j'ai posté la deuxième solution aujourd'hui. J'ai posté votre commentaire comme un avertissement, car vous l'avez assez bien formulé, puis-je vous citer?
Mong Zhu
J'ai écrit une troisième solution avec 2 boucles pour que la collection ne soit énumérée qu'une seule fois. la chose skip.take est une solution très inefficace
Mong Zhu
4

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;
        }
    }
user4698855
la source
1
Vous ne devez PAS réutiliser la variable batch. Vos consommateurs pourraient être complètement foutus par cela. Passez également le sizeparamètre à votre new Listpour optimiser sa taille.
ErikE
1
batch.Clear();batch = new List<T>();
Solution
3

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 Batchimplé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];
        }
    }
}
Theodor Zoulias
la source
2

Je rejoins ça très tard mais j'ai trouvé quelque chose de plus intéressant.

Nous pouvons donc l'utiliser ici Skipet Takepour 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

Kaushik
la source
1
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.
ErikE
@ErikE Cela dépend de ce que vous essayez d'accomplir. Si le traitement par lots n'est pas le problème et que vous avez juste besoin de diviser les éléments en plus petits morceaux pour le traitement, cela pourrait être la solution. J'utilise ceci pour MSCRM où il pourrait y avoir 100 enregistrements, ce qui n'est pas un problème pour LAMBDA à traiter par lots .. c'est l'économie qui prend quelques secondes ..
JensB
1
Bien sûr, il existe des cas d'utilisation où l'énumération complète n'a pas d'importance. Mais pourquoi écrire une méthode utilitaire de seconde classe alors que vous pouvez en écrire une superbe?
ErikE
Bonne alternative mais pas identique car le premier retourne une liste de listes vous permettant de parcourir en boucle.
Gareth Hopkins
passez 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.
Seabizkit du
2

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).

MrD chez KookerellaLtd
la source
1

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()
                {
                }
            }
        }
    }
leat
la source
1

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());
Johni Michels
la source
2
Cela peut être très inefficace, car le donné sourcesera répété très souvent.
Kevin Meier
1
Ce n'est pas seulement inefficace, mais pourrait également produire des résultats incorrects. Il n'y a aucune garantie qu'un énumérable produira les mêmes éléments lorsqu'il est énuméré deux fois. Prenez ce dénombrable comme exemple: Enumerable.Range(0, 1).SelectMany(_ => Enumerable.Range(0, new Random().Next())).
Theodor Zoulias
1

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 });
fracasser
la source
1

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();
fracasser
la source
Vous ne devriez jamais avoir à utiliser GetAwaiter().GetResult(). Il s'agit d'une odeur de code pour le code synchrone appelant avec force du code asynchrone.
gfache
-2
    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);
    }
Nichom
la source
Ajoutez une description / du texte dans votre réponse. Mettre uniquement du code peut signifier moins la plupart du temps.
Ariful Haque