Ergänzung nach sehr nützlichem Kommentar von mhand am Ende
Ursprüngliche Antwort
Obwohl die meisten Lösungen funktionieren könnten, denke ich, dass sie nicht sehr effizient sind. Angenommen, Sie möchten nur die ersten Elemente der ersten Teile. Dann möchten Sie nicht alle (zig) Elemente in Ihrer Sequenz durchlaufen.
Folgendes wird höchstens zweimal aufgezählt: einmal für den Take und einmal für den Skip. Es werden nicht mehr Elemente aufgelistet, als Sie verwenden werden:
public static IEnumerable<IEnumerable<TSource>> ChunkBy<TSource>
(this IEnumerable<TSource> source, int chunkSize)
{
while (source.Any()) // while there are elements left
{ // still something to chunk:
yield return source.Take(chunkSize); // return a chunk of chunkSize
source = source.Skip(chunkSize); // skip the returned chunk
}
}
Wie oft wird die Sequenz aufgelistet?
Angenommen, Sie teilen Ihre Quelle in Teile von chunkSize
. Sie zählen nur die ersten N Chunks auf. Von jedem aufgezählten Block werden nur die ersten M Elemente aufgelistet.
While(source.Any())
{
...
}
Das Any erhält den Enumerator, führt 1 MoveNext () aus und gibt den zurückgegebenen Wert zurück, nachdem der Enumerator entsorgt wurde. Dies wird N-mal durchgeführt
yield return source.Take(chunkSize);
Laut der Referenzquelle wird dies ungefähr Folgendes bewirken:
public static IEnumerable<TSource> Take<TSource>(this IEnumerable<TSource> source, int count)
{
return TakeIterator<TSource>(source, count);
}
static IEnumerable<TSource> TakeIterator<TSource>(IEnumerable<TSource> source, int count)
{
foreach (TSource element in source)
{
yield return element;
if (--count == 0) break;
}
}
Dies macht nicht viel, bis Sie anfangen, über den abgerufenen Chunk aufzuzählen. Wenn Sie mehrere Chunks abrufen, sich jedoch dafür entscheiden, nicht über den ersten Chunk aufzuzählen, wird der foreach nicht ausgeführt, wie Ihr Debugger Ihnen zeigt.
Wenn Sie sich entscheiden, die ersten M Elemente des ersten Blocks zu verwenden, wird die Ertragsrendite genau M Mal ausgeführt. Das heisst:
- Holen Sie sich den Enumerator
- Rufen Sie MoveNext () und Current M times auf.
- Entsorgen Sie den Enumerator
Nachdem der erste Block zurückgegeben wurde, überspringen wir diesen ersten Block:
source = source.Skip(chunkSize);
Noch einmal: Wir werfen einen Blick auf die Referenzquelle , um die zu findenskipiterator
static IEnumerable<TSource> SkipIterator<TSource>(IEnumerable<TSource> source, int count)
{
using (IEnumerator<TSource> e = source.GetEnumerator())
{
while (count > 0 && e.MoveNext()) count--;
if (count <= 0)
{
while (e.MoveNext()) yield return e.Current;
}
}
}
Wie Sie sehen, werden die SkipIterator
Aufrufe MoveNext()
für jedes Element im Chunk einmal ausgeführt. Es ruft nicht an Current
.
Pro Chunk sehen wir also, dass Folgendes getan wird:
- Any (): GetEnumerator; 1 MoveNext (); Enumerator entsorgen;
Nehmen():
- nichts, wenn der Inhalt des Blocks nicht aufgezählt ist.
Wenn der Inhalt aufgelistet ist: GetEnumerator (), ein MoveNext und ein Current pro aufgezähltem Element, Enpose enumerator;
Skip (): für jeden aufgezählten Block (NICHT den Inhalt des Blocks): GetEnumerator (), MoveNext () chunkSize times, no Current! Enumerator entsorgen
Wenn Sie sich ansehen, was mit dem Enumerator passiert, werden Sie feststellen, dass MoveNext () häufig aufgerufen wird und nur Current
die TSource-Elemente aufgerufen werden, auf die Sie tatsächlich zugreifen möchten.
Wenn Sie N Chunks der Größe chunkSize nehmen, rufen Sie MoveNext () auf
- N mal für Any ()
- Noch keine Zeit für Take, solange Sie die Chunks nicht aufzählen
- N mal chunkSize für Skip ()
Wenn Sie nur die ersten M Elemente jedes abgerufenen Blocks auflisten möchten, müssen Sie MoveNext M-mal pro aufgezähltem Block aufrufen.
Die Summe
MoveNext calls: N + N*M + N*chunkSize
Current calls: N*M; (only the items you really access)
Wenn Sie sich also dazu entschließen, alle Elemente aller Blöcke aufzulisten:
MoveNext: numberOfChunks + all elements + all elements = about twice the sequence
Current: every item is accessed exactly once
Ob MoveNext viel Arbeit ist oder nicht, hängt von der Art der Quellsequenz ab. Bei Listen und Arrays handelt es sich um ein einfaches Indexinkrement mit möglicherweise einer Überprüfung außerhalb des Bereichs.
Wenn Ihre IEnumerable jedoch das Ergebnis einer Datenbankabfrage ist, stellen Sie sicher, dass die Daten tatsächlich auf Ihrem Computer materialisiert sind. Andernfalls werden die Daten mehrmals abgerufen. DbContext und Dapper übertragen die Daten ordnungsgemäß an den lokalen Prozess, bevor auf sie zugegriffen werden kann. Wenn Sie dieselbe Sequenz mehrmals aufzählen, wird sie nicht mehrmals abgerufen. Dapper gibt ein Objekt zurück, das eine Liste ist. DbContext merkt sich, dass die Daten bereits abgerufen wurden.
Es hängt von Ihrem Repository ab, ob es sinnvoll ist, AsEnumerable () oder ToLists () aufzurufen, bevor Sie mit der Aufteilung der Elemente in Chunks beginnen