Antworten:
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();
}
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));
}
}
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
GroupBy
Aufzä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!
Wenn Sie mit sequence
definiert 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
}
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())
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
}
}
}
done
indem 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.Current
wenn i >= size
. Dadurch entfällt die Notwendigkeit, BatchInner
jeder Charge eine neue zuzuweisen .
i
Dies ist also nicht unbedingt effizienter als das Definieren einer separaten Klasse, aber meiner Meinung nach etwas sauberer.
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
source
Elemente auf und liefert sie, biscount
Elemente ergeben wurden odersource
keine Elemente mehr enthalten. Wenncount
die Anzahl der Elemente in überschritten wirdsource
, werden alle Elemente vonsource
zurü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 GetRange
Methode 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();
}
Skip
und Take
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. In Ihrem Beispiel haben Sie eine, List
die nicht zurückgestellt wird, daher ist dies weniger problematisch.
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;
}
}
size
Parameter außerdem an your new List
, um seine Größe zu optimieren.
batch.Clear();
durchbatch = new List<T>();
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 Batch
Implementierung 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];
}
}
}
Ich komme sehr spät dazu, aber ich fand etwas interessanter.
So können wir hier Skip
und Take
fü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
GroupBy
Aufzählung vollständig, bevor eine einzelne Zeile erstellt wird. Dies ist kein guter Weg, um Batching durchzuführen.
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.
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).
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()
{
}
}
}
}
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());
source
sehr oft wiederholt wird.
Enumerable.Range(0, 1).SelectMany(_ => Enumerable.Range(0, new Random().Next()))
.
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 });
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();
GetAwaiter().GetResult()
. Dies ist ein Codegeruch für synchronen Code, der zwangsweise asynchronen Code aufruft.
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);
}