Erstellen Sie Stapel in linq

104

Kann jemand einen Weg vorschlagen, um Stapel einer bestimmten Größe in linq zu erstellen?

Idealerweise möchte ich in der Lage sein, Operationen in Blöcken einer konfigurierbaren Menge auszuführen.

BlakeH
quelle

Antworten:

116

Sie müssen keinen Code schreiben. Verwenden Sie die MoreLINQ- Batch-Methode, mit der die Quellsequenz in Buckets mit einer Größe zusammengefasst wird (MoreLINQ ist als NuGet-Paket verfügbar, das Sie installieren können):

int size = 10;
var batches = sequence.Batch(size);

Welches implementiert ist als:

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
quelle
3
4 Bytes pro Artikel haben eine schreckliche Leistung ? Haben Sie einige Tests, die zeigen, was schrecklich bedeutet? Wenn Sie Millionen von Elementen in den Speicher laden, würde ich das nicht tun. Verwenden Sie serverseitiges Paging
Sergey Berezovskiy
4
Ich will Sie nicht beleidigen, aber es gibt einfachere Lösungen, die sich überhaupt nicht ansammeln. Darüber hinaus wird dies auch für nicht vorhandene Elemente Platz reservieren:Batch(new int[] { 1, 2 }, 1000000)
Nick Whaley
7
@ NickWhaley gut, stimme dir zu, dass zusätzlicher Platz zugewiesen wird, aber im wirklichen Leben hast du normalerweise genau die entgegengesetzte Situation - Liste von 1000 Artikeln, die in Chargen von 50 gehen sollten :)
Sergey Berezovskiy
1
Ja, die Situation sollte normalerweise umgekehrt sein, aber im wirklichen Leben können dies Benutzereingaben sein.
Nick Whaley
8
Dies ist eine vollkommen gute Lösung. Im wirklichen Leben: Sie validieren Benutzereingaben, behandeln Stapel als ganze Sammlungen von Elementen (die die Elemente sowieso ansammeln) und verarbeiten Stapel häufig parallel (was vom Iterator-Ansatz nicht unterstützt wird, und sind eine böse Überraschung, wenn Sie das nicht wissen Implementierungsdetails).
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));
    }
}

und die Verwendung wäre:

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));
}

AUSGABE:

0,1,2
3,4,5
6,7,8
9
PFUND
quelle
Arbeitete perfekt für mich
FunMatters
16
Sobald GroupByAufzählung beginnt, es muss nicht vollständig seine Quelle aufzuzählen? Dies verliert die verzögerte Bewertung der Quelle und damit in einigen Fällen den ganzen Vorteil der Dosierung!
ErikE
1
Wow, danke, du hast mich vor dem Wahnsinn gerettet. Funktioniert sehr gut
Riaan de Lange
3
Wie @ErikE erwähnt, listet diese Methode ihre Quelle vollständig auf, so dass sie, obwohl sie gut aussieht, den Zweck der faulen Auswertung / Pipelining
lasseschou
1
Tun Sie dies - es ist völlig angemessen, wenn Sie einen vorhandenen Block von Dingen für eine performante Verarbeitung in kleinere Stapel von Dingen aufteilen müssen. Die Alternative ist eine grobe Suchschleife, in der Sie die Stapel manuell aufteilen und dennoch die gesamte Quelle durchlaufen.
StingyJack
31

Wenn Sie mit sequencedefiniert als beginnen IEnumerable<T>und wissen, dass es sicher mehrfach aufgezählt werden kann (z. B. weil es sich um ein Array oder eine Liste handelt), können Sie dieses einfache Muster einfach verwenden, um die Elemente in Stapeln zu verarbeiten:

while (sequence.Any())
{
    var batch = sequence.Take(10);
    sequence = sequence.Skip(10);

    // do whatever you need to do with each batch here
}
Matthew Strawbridge
quelle
2
Schöne, einfache Möglichkeit zum Stapeln ohne viel Code oder Bedarf an externer Bibliothek
DevHawk
5
@ DevHawk: das ist es. Beachten Sie jedoch, dass die Leistung bei großen (r) Sammlungen exponentiell beeinträchtigt wird.
RobIII
28

Alle oben genannten Funktionen bieten eine hervorragende Leistung bei großen Stapeln oder geringem Speicherplatz. Musste meine eigene schreiben, die Pipeline wird (beachten Sie keine Artikelakkumulation irgendwo):

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());
    }
}

Bearbeiten: Bekanntes Problem bei diesem Ansatz ist, dass jeder Stapel vollständig aufgelistet und aufgelistet werden muss, bevor zum nächsten Stapel übergegangen wird. Zum Beispiel funktioniert das nicht:

//Select first item of every 100 items
Batch(list, 100).Select(b => b.First())
Nick Whaley
quelle
1
Die oben angegebene Routine @LB führt auch keine Artikelakkumulation durch.
Neontapir
2
@neontapir immer noch. Eine Münzsortiermaschine, die Ihnen zuerst Nickel und dann Groschen gibt, MUSS zuerst jede einzelne Münze überprüfen, bevor Sie einen Cent erhalten, um sicherzustellen, dass keine Nickel mehr vorhanden sind.
Nick Whaley
2
Ahhh ahha, ich habe Ihre Bearbeitungsnotiz verpasst, als ich mir diesen Code schnappte. Es dauerte einige Zeit, um zu verstehen, warum das Iterieren über nicht aufgezählte Stapel tatsächlich die gesamte ursprüngliche Sammlung (!!!) auflistete und X Stapel bereitstellte, die jeweils 1 Element aufgezählt hatten (wobei X die Anzahl der ursprünglichen Sammlungselemente ist).
eli
2
@NickWhaley Wenn ich Count () für das IEnumerable <IEnumerable <T>> -Ergebnis Ihres Codes mache, gibt es eine falsche Antwort, es gibt die Gesamtzahl der Elemente, wenn erwartet wird, ist die Gesamtzahl der erstellten Stapel. Dies ist nicht der Fall mit MoreLinq Batch-Code
Mrinal Kamboj
1
@ JohnZabroski - Hier ist eine kurze Zusammenfassung: gist.github.com/mmurrell/9225ed7c4d107c2195057f77e07f0f68
Matt Murrell
23

Dies ist eine völlig faule Implementierung von Batch mit geringem Overhead und einer Funktion, die keine Akkumulation durchführt. Basierend auf der Lösung von Nick Whaley (und behebt Probleme in dieser) mit Hilfe von EricRoller.

Die Iteration kommt direkt von der zugrunde liegenden IEnumerable, daher müssen Elemente in strikter Reihenfolge aufgelistet werden und dürfen nur einmal aufgerufen werden. Wenn einige Elemente nicht in einer inneren Schleife verbraucht werden, werden sie verworfen (und der Versuch, über einen gespeicherten Iterator erneut auf sie zuzugreifen, wird ausgelöst InvalidOperationException: Enumeration already finished.).

Sie können ein vollständiges Beispiel bei .NET Fiddle testen .

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
quelle
2
Dies ist die einzige vollständig faule Implementierung hier. In Übereinstimmung mit der Python-Implementierung itertools.GroupBy.
Eric Roller
1
Sie können für die Prüfung beseitigen , doneindem nur immer rufen e.Count()nach yield return e. Sie müssten die Schleife in BatchInner neu anordnen, um das undefinierte Verhalten nicht aufzurufen, source.Currentwenn i >= size. Dadurch entfällt die Notwendigkeit, BatchInnerjeder Charge eine neue zuzuweisen .
Eric Roller
1
Sie haben Recht, Sie müssen noch Informationen über den Fortschritt jeder Charge erfassen. Ich habe einen Fehler in Ihrem Code gefunden, wenn Sie versuchen, das zweite Element aus jeder Charge zu erhalten: Fehlergeige . Eine feste Implementierung ohne separate Klasse (mit C # 7) ist hier: Feste Geige . Beachten Sie, dass ich davon ausgehe, dass die CLR die lokale Funktion weiterhin einmal pro Schleife erstellt, um Variablen zu erfassen. iDies ist also nicht unbedingt effizienter als das Definieren einer separaten Klasse, aber meiner Meinung nach etwas sauberer.
Eric Roller
1
Ich habe diese Version mit BenchmarkDotNet mit System.Reactive.Linq.EnumerableEx.Buffer verglichen und Ihre Implementierung war 3-4, schneller, auf Sicherheitsrisiko. Intern weist EnumerableEx.Buffer eine Warteschlange der Liste <T> zu. Github.com/dotnet/reactive/blob/…
John Zabroski
1
Wenn Sie eine gepufferte Version davon wünschen, können Sie Folgendes tun: public static IEnumerable <IReadOnlyList <T>> BatchBuffered <T> (diese IEnumerable <T> -Quelle, int size) => Batch (Quelle, Größe) .Select (chunk = > (IReadOnlyList <T>) chunk.ToList ()); Die Verwendung von IReadOnlyList <T> soll den Benutzer darauf hinweisen, dass die Ausgabe zwischengespeichert wird. Sie können stattdessen auch IEnumerable <IEnumerable <T>> beibehalten.
Gfache
11

Ich frage mich, warum noch nie jemand eine For-Loop-Lösung der alten Schule veröffentlicht hat. Hier ist eine:

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);
}

Diese Einfachheit ist möglich, weil die Take-Methode:

... zählt sourceElemente auf und liefert sie, bis countElemente ergeben wurden oder sourcekeine Elemente mehr enthalten. Wenn countdie Anzahl der Elemente in überschritten wird source, werden alle Elemente von sourcezurückgegeben

Haftungsausschluss:

Die Verwendung von Überspringen und Nehmen innerhalb der Schleife bedeutet, dass die Aufzählung mehrmals aufgezählt wird. Dies ist gefährlich, wenn die Aufzählung zurückgestellt wird. Dies kann zu mehreren Ausführungen einer Datenbankabfrage, einer Webanforderung oder einer gelesenen Datei führen. Dieses Beispiel ist explizit für die Verwendung einer Liste vorgesehen, die nicht zurückgestellt wird, sodass es weniger problematisch ist. Es ist immer noch eine langsame Lösung, da überspringen die Sammlung bei jedem Aufruf auflistet.

Dies kann auch mit der GetRangeMethode gelöst werden , erfordert jedoch eine zusätzliche Berechnung, um eine mögliche Restcharge zu extrahieren:

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);
}

Hier ist eine dritte Möglichkeit, dies zu handhaben, die mit 2 Schleifen funktioniert. Dies stellt sicher, dass die Sammlung nur einmal aufgezählt wird!:

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
quelle
2
Sehr schöne Lösung. Die Leute haben vergessen, wie man eine Schleife benutzt
VitalickS
1
Die Verwendung von Skipund Takeinnerhalb der Schleife bedeutet, dass die Aufzählung mehrmals aufgezählt wird. Dies ist gefährlich, wenn die Aufzählung zurückgestellt wird. Dies kann zu mehreren Ausführungen einer Datenbankabfrage, einer Webanforderung oder einer gelesenen Datei führen. In Ihrem Beispiel haben Sie eine, Listdie nicht zurückgestellt wird, daher ist dies weniger problematisch.
Theodor Zoulias
@TheodorZoulias ja ich weiß, deshalb habe ich heute die zweite Lösung gepostet. Ich habe Ihren Kommentar als Haftungsausschluss veröffentlicht, weil Sie ihn recht gut formuliert haben. Soll ich Sie zitieren?
Mong Zhu
Ich habe eine dritte Lösung mit 2 Schleifen geschrieben, damit die Sammlung nur einmal aufgezählt wird. Die Sache mit skip.take ist eine sehr ineffiziente Lösung
Mong Zhu
4

Gleicher Ansatz wie MoreLINQ, jedoch mit List anstelle von Array. Ich habe kein Benchmarking durchgeführt, aber die Lesbarkeit ist für manche Menschen wichtiger:

    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
quelle
1
Sie sollten die Stapelvariable NICHT wiederverwenden. Ihre Verbraucher könnten dadurch völlig durcheinander geraten. Übergeben Sie den sizeParameter außerdem an your new List, um seine Größe zu optimieren.
ErikE
1
Einfache Lösung: Ersetzen batch.Clear();durchbatch = new List<T>();
NetMage
3

Hier ist ein Versuch, die faulen Implementierungen von Nick Whaley ( Link ) und Infogulch ( Link ) zu verbessern Batch. Dieser ist streng. Sie zählen die Stapel entweder in der richtigen Reihenfolge auf oder Sie erhalten eine Ausnahme.

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++;
            }
        }
    }
}

Und hier ist eine faule BatchImplementierung für Typquellen IList<T>. Dieser legt der Aufzählung keine Einschränkungen auf. Die Stapel können teilweise, in beliebiger Reihenfolge und mehrmals aufgelistet werden. Die Einschränkung, die Sammlung während der Aufzählung nicht zu ändern, besteht jedoch weiterhin. Dies wird erreicht, indem ein Dummy-Aufruf durchgeführt wird, enumerator.MoveNext()bevor ein Block oder Element ausgegeben wird. Der Nachteil ist, dass der Enumerator nicht verfügbar ist, da nicht bekannt ist, wann die Enumeration abgeschlossen sein wird.

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
quelle
2

Ich komme sehr spät dazu, aber ich fand etwas interessanter.

So können wir hier Skipund Takefür eine bessere Leistung verwenden.

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);
        }

    }

Als nächstes überprüfte ich mit 100000 Datensätzen. Nur die Schleife nimmt im Falle von mehr Zeit in AnspruchBatch

Code der Konsolenanwendung.

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;
}

Die benötigte Zeit ist so.

First - 00: 00: 00.0708, 00: 00: 00.0660

Zweitens (Take and Skip One) - 00: 00: 00.0008, 00: 00: 00.0008

Kaushik
quelle
1
GroupByAufzählung vollständig, bevor eine einzelne Zeile erstellt wird. Dies ist kein guter Weg, um Batching durchzuführen.
ErikE
@ErikE Das hängt davon ab, was Sie erreichen wollen. Wenn die Stapelverarbeitung nicht das Problem ist und Sie die Elemente nur für die Verarbeitung in kleinere Teile aufteilen müssen, ist dies möglicherweise genau das Richtige. Ich verwende dies für MSCRM, wo es möglicherweise 100 Datensätze gibt, was für LAMBDA kein Problem ist. Es ist die Speicherung, die Sekunden dauert.
JensB
1
Sicher, es gibt Anwendungsfälle, in denen die vollständige Aufzählung keine Rolle spielt. Aber warum eine Dienstprogrammmethode zweiter Klasse schreiben, wenn Sie eine hervorragende schreiben können?
ErikE
Gute Alternative, aber nicht identisch, da zuerst eine Liste von Listen zurückgegeben wird, die Sie durchlaufen können.
Gareth Hopkins
Wechseln Sie foreach (var batch in Ids2.Batch(5000))zu var gourpBatch = Ids2.Batch(5000)und überprüfen Sie die zeitgesteuerten Ergebnisse. oder füge tolist hinzu, var SecBatch = Ids2.Batch2(StartIndex, BatchSize);ich wäre interessiert, wenn sich deine Ergebnisse für das Timing ändern.
Seabizkit
2

Mit einem funktionierenden Hut erscheint dies also trivial ... aber in C # gibt es einige signifikante Nachteile.

Sie würden dies wahrscheinlich als eine Entfaltung von IEnumerable ansehen (googeln Sie es und Sie werden wahrscheinlich in einigen Haskell-Dokumenten landen, aber es kann einige F # -Stücke geben, die Entfaltung verwenden, wenn Sie F # kennen, blinzeln Sie auf die Haskell-Dokumente und es wird machen Sinn).

Unfold bezieht sich auf Fold ("Aggregat"), außer dass es nicht durch die Eingabe-IEnumerable iteriert, sondern durch die Ausgabedatenstrukturen (eine ähnliche Beziehung zwischen IEnumerable und IObservable). Tatsächlich denke ich, dass IObservable eine "Entfaltung" namens "generate" implementiert. ..)

Trotzdem brauchst du zuerst eine Entfaltungsmethode, ich denke das funktioniert (leider wird sie irgendwann den Stapel für große "Listen" sprengen ... du kannst dies sicher in F # schreiben, indem du Yield! anstatt Concat verwendest);

    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)));
    }

Dies ist etwas stumpf, da C # einige der Dinge, die funktionale Sprachen für selbstverständlich halten, nicht implementiert ... aber es benötigt im Grunde einen Startwert und generiert dann eine "Vielleicht" -Antwort des nächsten Elements in der IEnumerable und des nächsten Startwerts (Vielleicht) existiert nicht in C #, daher haben wir IEnumerable verwendet, um es zu fälschen) und verketten den Rest der Antwort (ich kann nicht für die Komplexität von "O (n?)" bürgen).

Sobald Sie das getan haben, dann;

    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);
    }

es sieht alles ziemlich sauber aus ... Sie nehmen die "n" -Elemente als "nächstes" Element in der IEnumerable und das "Ende" ist der Rest der unverarbeiteten Liste.

Wenn sich nichts im Kopf befindet ... bist du vorbei ... gibst du "Nichts" zurück (aber als leere IEnumerable gefälscht>) ... sonst gibst du das Kopfelement und den Schwanz zur Verarbeitung zurück.

Sie können dies wahrscheinlich mit IObservable tun, es gibt wahrscheinlich bereits eine "Batch" -ähnliche Methode, und Sie können diese wahrscheinlich verwenden.

Wenn das Risiko eines Stapelüberlaufs besorgniserregend ist (sollte es wahrscheinlich sein), sollten Sie es in F # implementieren (und es gibt wahrscheinlich bereits eine F # -Bibliothek (FSharpX?) Damit).

(Ich habe nur einige rudimentäre Tests durchgeführt, daher gibt es möglicherweise die seltsamen Fehler darin).

MrD bei KookerellaLtd
quelle
1

Ich habe eine benutzerdefinierte IEnumerable-Implementierung geschrieben, die ohne linq funktioniert und eine einzelne Aufzählung über die Daten garantiert. All dies wird auch erreicht, ohne dass Sicherungslisten oder Arrays erforderlich sind, die Speicherexplosionen über große Datenmengen verursachen.

Hier sind einige grundlegende Tests:

    [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);
    }

Die Erweiterungsmethode zum Partitionieren der Daten.

/// <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);
    }
}

Dies ist die implementierende Klasse

    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
quelle
1

Ich weiß, dass jeder komplexe Systeme verwendet hat, um diese Arbeit zu erledigen, und ich verstehe wirklich nicht, warum. Durch Übernehmen und Überspringen können alle diese Vorgänge mithilfe der allgemeinen Auswahlfunktion mit Func<TSource,Int32,TResult>Transformationsfunktion ausgeführt werden. Mögen:

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
quelle
2
Dies kann sehr ineffizient sein, da das Gegebene sourcesehr oft wiederholt wird.
Kevin Meier
1
Dies ist nicht nur ineffizient, sondern kann auch zu falschen Ergebnissen führen. Es gibt keine Garantie dafür, dass eine Aufzählung bei zweimaliger Aufzählung dieselben Elemente liefert. Nehmen Sie diese Aufzählung als Beispiel : Enumerable.Range(0, 1).SelectMany(_ => Enumerable.Range(0, new Random().Next())).
Theodor Zoulias
1

Nur eine weitere einzeilige Implementierung. Dies funktioniert auch mit einer leeren Liste. In diesem Fall erhalten Sie eine Stapelsammlung mit der Größe Null.

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 });
frhack
quelle
1

Eine andere Möglichkeit ist die Verwendung des Rx Buffer-Operators

//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();
frhack
quelle
Sie sollten nie verwenden müssen GetAwaiter().GetResult(). Dies ist ein Codegeruch für synchronen Code, der zwangsweise asynchronen Code aufruft.
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
quelle
Fügen Sie Ihrer Antwort eine Beschreibung / einen Text hinzu. Das Einfügen von nur Code kann die meiste Zeit weniger bedeuten.
Ariful Haque