Liste mit LINQ in Unterlisten aufteilen

377

Gibt es eine Möglichkeit, a List<SomeObject>in mehrere separate Listen zu unterteilen SomeObject, wobei der Elementindex als Begrenzer für jeden Split verwendet wird?

Lassen Sie mich veranschaulichen:

Ich habe ein List<SomeObject>und ich brauche ein List<List<SomeObject>>oder List<SomeObject>[], damit jede dieser resultierenden Listen eine Gruppe von 3 Elementen der ursprünglichen Liste enthält (nacheinander).

z.B.:

  • Originalliste: [a, g, e, w, p, s, q, f, x, y, i, m, c]

  • Resultierende Listen: [a, g, e], [w, p, s], [q, f, x], [y, i, m], [c]

Ich würde auch die resultierende Listengröße benötigen, um ein Parameter dieser Funktion zu sein.

Felipe Lima
quelle

Antworten:

378

Versuchen Sie den folgenden Code.

public static IList<IList<T>> Split<T>(IList<T> source)
{
    return  source
        .Select((x, i) => new { Index = i, Value = x })
        .GroupBy(x => x.Index / 3)
        .Select(x => x.Select(v => v.Value).ToList())
        .ToList();
}

Die Idee ist, die Elemente zuerst nach Indizes zu gruppieren. Das Teilen durch drei bewirkt, dass sie in Gruppen von 3 gruppiert werden. Konvertieren Sie dann jede Gruppe in eine Liste und die IEnumerablevon Listin eine Listvon Lists

JaredPar
quelle
21
GroupBy führt eine implizite Sortierung durch. Das kann die Leistung beeinträchtigen. Was wir brauchen, ist eine Art Umkehrung von SelectMany.
Yfeldblum
5
@Justice, GroupBy kann durch Hashing implementiert werden. Woher wissen Sie, dass die Implementierung von GroupBy "die Leistung beeinträchtigen kann"?
Amy B
5
GroupBy gibt nichts zurück, bis alle Elemente aufgelistet sind. Deshalb ist es langsam. Die Listen, die OP wünscht, sind zusammenhängend, sodass eine bessere Methode die erste Unterliste ergeben könnte, [a,g,e]bevor weitere Teile der ursprünglichen Liste aufgelistet werden.
Colonel Panic
9
Nehmen Sie das extreme Beispiel einer unendlichen IEnumerable. GroupBy(x=>f(x)).First()wird niemals eine Gruppe ergeben. OP fragte nach Listen, aber wenn wir schreiben, um mit IEnumerable zu arbeiten und nur eine einzige Iteration durchführen, profitieren wir vom Leistungsvorteil.
Colonel Panic
8
@ Nick Order bleibt jedoch nicht erhalten. Es ist immer noch gut zu wissen, aber Sie würden sie in (0,3,6,9, ...), (1,4,7,10, ...), (2,5,8) gruppieren , 11, ...). Wenn die Reihenfolge keine Rolle spielt, ist es in Ordnung, aber in diesem Fall klingt es so, als wäre es wichtig.
Reafexus
325

Diese Frage ist etwas alt, aber ich habe sie gerade geschrieben, und ich denke, sie ist etwas eleganter als die anderen vorgeschlagenen Lösungen:

/// <summary>
/// Break a list of items into chunks of a specific size
/// </summary>
public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize)
{
    while (source.Any())
    {
        yield return source.Take(chunksize);
        source = source.Skip(chunksize);
    }
}
CaseyB
quelle
14
Ich liebe diese Lösung. Ich würde empfehlen, diese Überprüfung der if (chunksize <= 0) throw new ArgumentException("Chunk size must be greater than zero.", "chunksize");
geistigen Gesundheit
10
Ich mag das, aber es ist nicht super effizient
Sam Saffron
51
Ich mag dieses, aber Zeiteffizienz ist O(n²). Sie können die Liste durchlaufen und eine O(n)Zeit abrufen .
HPIPP
8
@hIpPy, wie ist es n ^ 2? Sieht für mich linear aus
Vivek Maharajh
13
@vivekmaharajh sourcewird IEnumerablejedes Mal durch ein verpacktes ersetzt . Das Nehmen von Elementen aus sourcegeht also durch Schichten von Skips
Lasse Espeholt
99

Im Allgemeinen funktioniert der von CaseyB vorgeschlagene Ansatz einwandfrei. Wenn Sie einen übergeben List<T>, ist es schwer , daran etwas auszusetzen. Vielleicht würde ich ihn ändern in:

public static IEnumerable<IEnumerable<T>> ChunkTrivialBetter<T>(this IEnumerable<T> source, int chunksize)
{
   var pos = 0; 
   while (source.Skip(pos).Any())
   {
      yield return source.Skip(pos).Take(chunksize);
      pos += chunksize;
   }
}

Das vermeidet massive Anrufketten. Dieser Ansatz weist jedoch einen allgemeinen Fehler auf. Es werden zwei Aufzählungen pro Block ausgeführt, um das Problem hervorzuheben, das beim Ausführen ausgeführt wird:

foreach (var item in Enumerable.Range(1, int.MaxValue).Chunk(8).Skip(100000).First())
{
   Console.WriteLine(item);
}
// wait forever 

Um dies zu überwinden, können wir Camerons Ansatz ausprobieren , der den obigen Test in Bravour besteht, da er die Aufzählung nur einmal durchläuft.

Das Problem ist, dass es einen anderen Fehler hat, es materialisiert jedes Element in jedem Block. Das Problem bei diesem Ansatz ist, dass Sie über genügend Speicher verfügen.

Um dies zu veranschaulichen, versuchen Sie Folgendes auszuführen:

foreach (var item in Enumerable.Range(1, int.MaxValue)
               .Select(x => x + new string('x', 100000))
               .Clump(10000).Skip(100).First())
{
   Console.Write('.');
}
// OutOfMemoryException

Schließlich sollte jede Implementierung in der Lage sein, Iterationen von Chunks außerhalb der Reihenfolge zu verarbeiten, zum Beispiel:

Enumerable.Range(1,3).Chunk(2).Reverse().ToArray()
// should return [3],[1,2]

Viele höchst optimale Lösungen wie meine erste Überarbeitung dieser Antwort sind dort gescheitert. Das gleiche Problem ist in der optimierten Antwort von casperOne zu sehen .

Um all diese Probleme zu beheben, können Sie Folgendes verwenden:

namespace ChunkedEnumerator
{
    public static class Extensions 
    {
        class ChunkedEnumerable<T> : IEnumerable<T>
        {
            class ChildEnumerator : IEnumerator<T>
            {
                ChunkedEnumerable<T> parent;
                int position;
                bool done = false;
                T current;


                public ChildEnumerator(ChunkedEnumerable<T> parent)
                {
                    this.parent = parent;
                    position = -1;
                    parent.wrapper.AddRef();
                }

                public T Current
                {
                    get
                    {
                        if (position == -1 || done)
                        {
                            throw new InvalidOperationException();
                        }
                        return current;

                    }
                }

                public void Dispose()
                {
                    if (!done)
                    {
                        done = true;
                        parent.wrapper.RemoveRef();
                    }
                }

                object System.Collections.IEnumerator.Current
                {
                    get { return Current; }
                }

                public bool MoveNext()
                {
                    position++;

                    if (position + 1 > parent.chunkSize)
                    {
                        done = true;
                    }

                    if (!done)
                    {
                        done = !parent.wrapper.Get(position + parent.start, out current);
                    }

                    return !done;

                }

                public void Reset()
                {
                    // per http://msdn.microsoft.com/en-us/library/system.collections.ienumerator.reset.aspx
                    throw new NotSupportedException();
                }
            }

            EnumeratorWrapper<T> wrapper;
            int chunkSize;
            int start;

            public ChunkedEnumerable(EnumeratorWrapper<T> wrapper, int chunkSize, int start)
            {
                this.wrapper = wrapper;
                this.chunkSize = chunkSize;
                this.start = start;
            }

            public IEnumerator<T> GetEnumerator()
            {
                return new ChildEnumerator(this);
            }

            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }

        }

        class EnumeratorWrapper<T>
        {
            public EnumeratorWrapper (IEnumerable<T> source)
            {
                SourceEumerable = source;
            }
            IEnumerable<T> SourceEumerable {get; set;}

            Enumeration currentEnumeration;

            class Enumeration
            {
                public IEnumerator<T> Source { get; set; }
                public int Position { get; set; }
                public bool AtEnd { get; set; }
            }

            public bool Get(int pos, out T item) 
            {

                if (currentEnumeration != null && currentEnumeration.Position > pos)
                {
                    currentEnumeration.Source.Dispose();
                    currentEnumeration = null;
                }

                if (currentEnumeration == null)
                {
                    currentEnumeration = new Enumeration { Position = -1, Source = SourceEumerable.GetEnumerator(), AtEnd = false };
                }

                item = default(T);
                if (currentEnumeration.AtEnd)
                {
                    return false;
                }

                while(currentEnumeration.Position < pos) 
                {
                    currentEnumeration.AtEnd = !currentEnumeration.Source.MoveNext();
                    currentEnumeration.Position++;

                    if (currentEnumeration.AtEnd) 
                    {
                        return false;
                    }

                }

                item = currentEnumeration.Source.Current;

                return true;
            }

            int refs = 0;

            // needed for dispose semantics 
            public void AddRef()
            {
                refs++;
            }

            public void RemoveRef()
            {
                refs--;
                if (refs == 0 && currentEnumeration != null)
                {
                    var copy = currentEnumeration;
                    currentEnumeration = null;
                    copy.Source.Dispose();
                }
            }
        }

        public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize)
        {
            if (chunksize < 1) throw new InvalidOperationException();

            var wrapper =  new EnumeratorWrapper<T>(source);

            int currentPos = 0;
            T ignore;
            try
            {
                wrapper.AddRef();
                while (wrapper.Get(currentPos, out ignore))
                {
                    yield return new ChunkedEnumerable<T>(wrapper, chunksize, currentPos);
                    currentPos += chunksize;
                }
            }
            finally
            {
                wrapper.RemoveRef();
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            int i = 10;
            foreach (var group in Enumerable.Range(1, int.MaxValue).Skip(10000000).Chunk(3))
            {
                foreach (var n in group)
                {
                    Console.Write(n);
                    Console.Write(" ");
                }
                Console.WriteLine();
                if (i-- == 0) break;
            }


            var stuffs = Enumerable.Range(1, 10).Chunk(2).ToArray();

            foreach (var idx in new [] {3,2,1})
            {
                Console.Write("idx " + idx + " ");
                foreach (var n in stuffs[idx])
                {
                    Console.Write(n);
                    Console.Write(" ");
                }
                Console.WriteLine();
            }

            /*

10000001 10000002 10000003
10000004 10000005 10000006
10000007 10000008 10000009
10000010 10000011 10000012
10000013 10000014 10000015
10000016 10000017 10000018
10000019 10000020 10000021
10000022 10000023 10000024
10000025 10000026 10000027
10000028 10000029 10000030
10000031 10000032 10000033
idx 3 7 8
idx 2 5 6
idx 1 3 4
             */

            Console.ReadKey();


        }

    }
}

Es gibt auch eine Reihe von Optimierungen, die Sie für die Iteration von Chunks außerhalb der Reihenfolge einführen können, was hier nicht möglich ist.

Welche Methode sollten Sie wählen? Es hängt ganz von dem Problem ab, das Sie lösen möchten. Wenn Sie sich nicht mit dem ersten Fehler befassen, ist die einfache Antwort unglaublich ansprechend.

Beachten Sie, wie bei den meisten Methoden, dass dies für Multithreading nicht sicher ist. Dinge können seltsam werden, wenn Sie es threadsicher machen möchten, die Sie ändern müssten EnumeratorWrapper.

Sam Safran
quelle
Wäre der Fehler Enumerable.Range (0, 100) .Chunk (3) .Reverse (). ToArray () falsch oder Enumerable.Range (0, 100) .ToArray (). Chunk (3) .Reverse () .ToArray () eine Ausnahme auslösen?
Cameron MacFarland
@SamSaffron Ich habe meine Antwort aktualisiert und den Code für den meiner Meinung nach wichtigsten Anwendungsfall enorm vereinfacht (und die Vorbehalte anerkannt).
CasperOne
Was ist mit IQueryable <> chuncking? Ich vermute, dass ein Take / Skip-Ansatz optimal wäre, wenn wir ein Maximum der Operationen an den Anbieter delegieren möchten
Guillaume86
@ Guillaume86 Ich stimme zu, wenn Sie eine IList oder IQueryable haben, können Sie alle Arten von Verknüpfungen verwenden, die dies viel schneller machen würden (Linq macht dies intern für alle Arten von anderen Methoden)
Sam Saffron
1
Dies ist bei weitem die beste Antwort für Effizienz. Ich habe ein Problem bei der Verwendung von SqlBulkCopy mit einer IEnumerable, die zusätzliche Prozesse für jede Spalte ausführt, sodass sie mit nur einem Durchgang effizient ausgeführt werden muss. Auf diese Weise kann ich die IEnumerable in überschaubare Blöcke aufteilen. (Für diejenigen, die sich fragen, habe ich den Streaming-Modus von SqlBulkCopy aktiviert, der anscheinend defekt ist.)
Brain2000
64

Sie könnten eine Reihe von Abfragen verwenden, die Takeund verwenden Skip, aber das würde meiner Meinung nach zu viele Iterationen zur ursprünglichen Liste hinzufügen.

Ich denke eher, Sie sollten einen eigenen Iterator erstellen, wie folgt:

public static IEnumerable<IEnumerable<T>> GetEnumerableOfEnumerables<T>(
  IEnumerable<T> enumerable, int groupSize)
{
   // The list to return.
   List<T> list = new List<T>(groupSize);

   // Cycle through all of the items.
   foreach (T item in enumerable)
   {
     // Add the item.
     list.Add(item);

     // If the list has the number of elements, return that.
     if (list.Count == groupSize)
     {
       // Return the list.
       yield return list;

       // Set the list to a new list.
       list = new List<T>(groupSize);
     }
   }

   // Return the remainder if there is any,
   if (list.Count != 0)
   {
     // Return the list.
     yield return list;
   }
}

Sie können dies dann aufrufen und es ist LINQ-fähig, damit Sie andere Operationen an den resultierenden Sequenzen ausführen können.


Angesichts von Sams Antwort hatte ich das Gefühl, dass es einen einfacheren Weg gibt, dies zu tun, ohne:

  • Wiederholen der Liste (was ich ursprünglich nicht getan habe)
  • Materialisieren der Elemente in Gruppen vor dem Freigeben des Blocks (bei großen Teilen von Elementen treten Speicherprobleme auf)
  • Der gesamte Code, den Sam gepostet hat

Das heißt, hier ist ein weiterer Pass, die ich in eine Erweiterungsmethode zu kodifizieren haben IEnumerable<T>genannt Chunk:

public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, 
    int chunkSize)
{
    // Validate parameters.
    if (source == null) throw new ArgumentNullException("source");
    if (chunkSize <= 0) throw new ArgumentOutOfRangeException("chunkSize",
        "The chunkSize parameter must be a positive value.");

    // Call the internal implementation.
    return source.ChunkInternal(chunkSize);
}

Nichts Überraschendes, nur grundlegende Fehlerprüfung.

Weiter zu ChunkInternal:

private static IEnumerable<IEnumerable<T>> ChunkInternal<T>(
    this IEnumerable<T> source, int chunkSize)
{
    // Validate parameters.
    Debug.Assert(source != null);
    Debug.Assert(chunkSize > 0);

    // Get the enumerator.  Dispose of when done.
    using (IEnumerator<T> enumerator = source.GetEnumerator())
    do
    {
        // Move to the next element.  If there's nothing left
        // then get out.
        if (!enumerator.MoveNext()) yield break;

        // Return the chunked sequence.
        yield return ChunkSequence(enumerator, chunkSize);
    } while (true);
}

Grundsätzlich erhält es das IEnumerator<T>und iteriert manuell durch jedes Element. Es wird geprüft, ob derzeit Elemente aufgezählt werden müssen. Wenn nach der Durchzählung jedes Blocks keine Elemente mehr vorhanden sind, bricht er aus.

Sobald festgestellt wird, dass sich Elemente in der Sequenz befinden, delegiert es die Verantwortung für die innere IEnumerable<T>Implementierung an ChunkSequence:

private static IEnumerable<T> ChunkSequence<T>(IEnumerator<T> enumerator, 
    int chunkSize)
{
    // Validate parameters.
    Debug.Assert(enumerator != null);
    Debug.Assert(chunkSize > 0);

    // The count.
    int count = 0;

    // There is at least one item.  Yield and then continue.
    do
    {
        // Yield the item.
        yield return enumerator.Current;
    } while (++count < chunkSize && enumerator.MoveNext());
}

Da MoveNextbereits bei der IEnumerator<T>Übergabe an aufgerufen wurde ChunkSequence, wird das zurückgegebene Element ausgegeben Currentund dann die Anzahl erhöht, wobei sichergestellt wird, dass niemals mehr als chunkSizeElemente zurückgegeben werden und nach jeder Iteration zum nächsten Element in der Sequenz gewechselt wird (jedoch kurzgeschlossen wird, wenn die Anzahl von Die erhaltenen Gegenstände überschreiten die Blockgröße.

Wenn keine Elemente mehr vorhanden sind, führt die InternalChunkMethode einen weiteren Durchlauf in der äußeren Schleife durch. Wenn sie MoveNextjedoch ein zweites Mal aufgerufen wird, wird gemäß der Dokumentation (Hervorhebung von mir) immer noch false zurückgegeben :

Wenn MoveNext das Ende der Auflistung überschreitet, wird der Enumerator nach dem letzten Element in der Auflistung positioniert und MoveNext gibt false zurück. Wenn sich der Enumerator an dieser Position befindet, geben nachfolgende Aufrufe von MoveNext ebenfalls false zurück, bis Reset aufgerufen wird.

Zu diesem Zeitpunkt wird die Schleife unterbrochen und die Sequenz von Sequenzen wird beendet.

Dies ist ein einfacher Test:

static void Main()
{
    string s = "agewpsqfxyimc";

    int count = 0;

    // Group by three.
    foreach (IEnumerable<char> g in s.Chunk(3))
    {
        // Print out the group.
        Console.Write("Group: {0} - ", ++count);

        // Print the items.
        foreach (char c in g)
        {
            // Print the item.
            Console.Write(c + ", ");
        }

        // Finish the line.
        Console.WriteLine();
    }
}

Ausgabe:

Group: 1 - a, g, e,
Group: 2 - w, p, s,
Group: 3 - q, f, x,
Group: 4 - y, i, m,
Group: 5 - c,

Ein wichtiger Hinweis: Dies funktioniert nicht , wenn Sie nicht die gesamte untergeordnete Sequenz entleeren oder an einem beliebigen Punkt in der übergeordneten Sequenz unterbrechen. Dies ist eine wichtige Einschränkung, aber wenn Ihr Anwendungsfall darin besteht, dass Sie jedes Element der Sequenz von Sequenzen verbrauchen , funktioniert dies für Sie.

Außerdem wird es seltsame Dinge tun, wenn Sie mit der Reihenfolge spielen, so wie es Sam an einem Punkt getan hat .

casperOne
quelle
Ich denke, dies ist die beste Lösung ... das einzige Problem ist, dass die Liste keine Länge hat ... sie hat die Anzahl. Aber das ist leicht zu ändern. Wir können dies verbessern, indem wir nicht einmal Listen erstellen, sondern ienumerables zurückgeben, die Verweise auf die Hauptliste mit einer Kombination aus Versatz und Länge enthalten. Wenn die Gruppengröße also groß ist, verschwenden wir keinen Speicher. Kommentar, wenn ich es aufschreiben soll.
Amir
@Amir Ich würde gerne sehen, dass geschrieben
Samandmoore
Das ist nett und schnell - Cameron hat auch nach Ihrer eine sehr ähnliche veröffentlicht. Die einzige Einschränkung ist, dass sie Chunks puffert. Dies kann zu Speichermangel führen, wenn Chunks und Artikelgrößen groß sind. Siehe meine Antwort für eine alternative, wenn auch viel haarigere Antwort.
Sam Saffron
@SamSaffron Ja, wenn Sie eine große Anzahl von Elementen in der haben List<T>, werden Sie aufgrund der Pufferung offensichtlich Speicherprobleme haben. Rückblickend hätte ich das in der Antwort vermerken sollen, aber es schien zu der Zeit, als der Fokus auf zu vielen Iterationen lag. Das heißt, Ihre Lösung ist in der Tat haariger. Ich habe es nicht getestet, aber jetzt frage ich mich, ob es eine weniger haarige Lösung gibt.
CasperOne
@casperOne yeah ... Google hat mir diese Seite gegeben, als ich nach einer Möglichkeit gesucht habe, Aufzählungen aufzuteilen. Für meinen speziellen Anwendungsfall teile ich eine wahnsinnig große Liste von Datensätzen auf, die von der Datenbank zurückgegeben werden, wenn ich sie in a materialisiere Liste es würde explodieren (in der Tat hat dapper einen Puffer: falsche Option nur für diesen Anwendungsfall)
Sam Saffron
48

Ok, hier ist meine Meinung dazu:

  • völlig faul: arbeitet mit unendlich vielen Aufzählungen
  • kein Zwischenkopieren / Puffern
  • O (n) Ausführungszeit
  • funktioniert auch, wenn innere Sequenzen nur teilweise verbraucht werden

public static IEnumerable<IEnumerable<T>> Chunks<T>(this IEnumerable<T> enumerable,
                                                    int chunkSize)
{
    if (chunkSize < 1) throw new ArgumentException("chunkSize must be positive");

    using (var e = enumerable.GetEnumerator())
    while (e.MoveNext())
    {
        var remaining = chunkSize;    // elements remaining in the current chunk
        var innerMoveNext = new Func<bool>(() => --remaining > 0 && e.MoveNext());

        yield return e.GetChunk(innerMoveNext);
        while (innerMoveNext()) {/* discard elements skipped by inner iterator */}
    }
}

private static IEnumerable<T> GetChunk<T>(this IEnumerator<T> e,
                                          Func<bool> innerMoveNext)
{
    do yield return e.Current;
    while (innerMoveNext());
}

Beispiel Verwendung

var src = new [] {1, 2, 3, 4, 5, 6}; 

var c3 = src.Chunks(3);      // {{1, 2, 3}, {4, 5, 6}}; 
var c4 = src.Chunks(4);      // {{1, 2, 3, 4}, {5, 6}}; 

var sum   = c3.Select(c => c.Sum());    // {6, 15}
var count = c3.Count();                 // 2
var take2 = c3.Select(c => c.Take(2));  // {{1, 2}, {4, 5}}

Erklärungen

Der Code verschachtelt zwei yieldbasierte Iteratoren.

Der äußere Iterator muss verfolgen, wie viele Elemente vom inneren (Block-) Iterator effektiv verbraucht wurden. Dies erfolgt durch Schließen remainingmit innerMoveNext(). Nicht verbrauchte Elemente eines Blocks werden verworfen, bevor der nächste Block vom äußeren Iterator ausgegeben wird. Dies ist notwendig, da Sie sonst inkonsistente Ergebnisse erhalten, wenn die inneren Aufzählungen nicht (vollständig) verbraucht sind (z. B. c3.Count()würde 6 zurückgeben).

Hinweis: Die Antwort wurde aktualisiert, um die von @aolszowka aufgezeigten Mängel zu beheben.

3dGrabber
quelle
2
Sehr schön. Meine "richtige" Lösung war viel komplizierter. Dies ist meiner Meinung nach die Antwort Nr. 1.
CaseyB
Dies leidet unter unerwartetem (aus API-Sicht) Verhalten, wenn ToArray () aufgerufen wird. Es ist auch nicht threadsicher.
Aolszowka
@aolszowka: Könnten Sie bitte näher darauf eingehen?
3dGrabber
@ 3dGrabber Vielleicht war es so, wie ich Ihren Code neu faktorisiert habe (sorry, es ist ein bisschen zu lang, um hier vorbei zu sein, im Grunde anstelle einer Erweiterungsmethode, die ich im sourceEnumerator übergeben habe). Der Testfall, den ich verwendet habe, war etwas in diesem Sinne: int [] arrayToSort = new int [] {9, 7, 2, 6, 3, 4, 8, 5, 1, 10, 11, 12, 13}; var source = Chunkify <int> (arrayToSort, 3) .ToArray (); Das Ergebnis war Quelle, was anzeigt, dass 13 Blöcke vorhanden waren (die Anzahl der Elemente). Dies ergab für mich einen Sinn, da der Enumerator nicht inkrementiert wurde, es sei denn, Sie haben die inneren Aufzählungen abgefragt.
Aolszowka
1
@ aolszowka: sehr gültige Punkte. Ich habe eine Warnung und einen Verwendungsabschnitt hinzugefügt. Der Code setzt voraus, dass Sie über die innere Aufzählung iterieren. Mit Ihrer Lösung verlieren Sie jedoch die Faulheit. Ich denke, es sollte möglich sein, das Beste aus beiden Welten mit einem benutzerdefinierten IEnumerator zwischenzuspeichern. Wenn ich eine Lösung finde, werde ich sie hier
posten
18

völlig faul, kein Zählen oder Kopieren:

public static class EnumerableExtensions
{

  public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> source, int len)
  {
     if (len == 0)
        throw new ArgumentNullException();

     var enumer = source.GetEnumerator();
     while (enumer.MoveNext())
     {
        yield return Take(enumer.Current, enumer, len);
     }
  }

  private static IEnumerable<T> Take<T>(T head, IEnumerator<T> tail, int len)
  {
     while (true)
     {
        yield return head;
        if (--len == 0)
           break;
        if (tail.MoveNext())
           head = tail.Current;
        else
           break;
     }
  }
}
xtofs
quelle
Diese Lösung ist so elegant, dass es mir leid tut, dass ich diese Antwort nicht mehr als einmal bewerten kann.
Mark
3
Ich glaube nicht, dass dies jemals genau scheitern würde. Aber es könnte sicherlich ein merkwürdiges Verhalten haben. Wenn Sie 100 Artikel hatten und sich in 10er-Chargen
aufteilten
1
Wie @CaseyB erwähnt hat, leidet dies unter dem gleichen fehlerhaften 3dGrabber , der hier angesprochen wird stackoverflow.com/a/20953521/1037948 , aber Mann, ist es schnell!
Drzaus
1
Dies ist eine schöne Lösung. Tut genau das, was es verspricht.
Rod Hartzell
Mit Abstand die eleganteste und auf den Punkt gebrachte Lösung. Das einzige ist, Sie sollten eine Prüfung auf negative Zahlen hinzufügen und die ArgumentNullException durch eine ArgumentException
Romain Vergnory
13

Ich denke, der folgende Vorschlag wäre der schnellste. Ich opfere die Faulheit der Quelle Enumerable für die Fähigkeit, Array.Copy zu verwenden und die Länge jeder meiner Unterlisten im Voraus zu kennen.

public static IEnumerable<T[]> Chunk<T>(this IEnumerable<T> items, int size)
{
    T[] array = items as T[] ?? items.ToArray();
    for (int i = 0; i < array.Length; i+=size)
    {
        T[] chunk = new T[Math.Min(size, array.Length - i)];
        Array.Copy(array, i, chunk, 0, chunk.Length);
        yield return chunk;
    }
}
Marc-André Bertrand
quelle
Es ist nicht nur am schnellsten, sondern verarbeitet auch weitere aufzählbare Operationen für das Ergebnis korrekt, z. B. items.Chunk (5) .Reverse (). SelectMany (x => x)
auch
9

Wir können die Lösung von @ JaredPar verbessern, um eine echte faule Bewertung durchzuführen. Wir verwenden eine GroupAdjacentByMethode, die Gruppen aufeinanderfolgender Elemente mit demselben Schlüssel ergibt:

sequence
.Select((x, i) => new { Value = x, Index = i })
.GroupAdjacentBy(x=>x.Index/3)
.Select(g=>g.Select(x=>x.Value))

Da die Gruppen einzeln erhalten werden, arbeitet diese Lösung effizient mit langen oder unendlichen Sequenzen.

Oberst Panik
quelle
8

Ich habe vor einigen Jahren eine Clump-Erweiterungsmethode geschrieben. Funktioniert hervorragend und ist hier die schnellste Implementierung. : P.

/// <summary>
/// Clumps items into same size lots.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source">The source list of items.</param>
/// <param name="size">The maximum size of the clumps to make.</param>
/// <returns>A list of list of items, where each list of items is no bigger than the size given.</returns>
public static IEnumerable<IEnumerable<T>> Clump<T>(this IEnumerable<T> source, int size)
{
    if (source == null)
        throw new ArgumentNullException("source");
    if (size < 1)
        throw new ArgumentOutOfRangeException("size", "size must be greater than 0");

    return ClumpIterator<T>(source, size);
}

private static IEnumerable<IEnumerable<T>> ClumpIterator<T>(IEnumerable<T> source, int size)
{
    Debug.Assert(source != null, "source is null.");

    T[] items = new T[size];
    int count = 0;
    foreach (var item in source)
    {
        items[count] = item;
        count++;

        if (count == size)
        {
            yield return items;
            items = new T[size];
            count = 0;
        }
    }
    if (count > 0)
    {
        if (count == size)
            yield return items;
        else
        {
            T[] tempItems = new T[count];
            Array.Copy(items, tempItems, count);
            yield return tempItems;
        }
    }
}
Cameron MacFarland
quelle
es sollte funktionieren, aber es puffert 100% der Brocken, ich habe versucht, das zu vermeiden ... aber es stellt sich als unglaublich haarig heraus.
Sam Saffron
@ SamSaffron Ja. Vor allem, wenn Sie Dinge wie plinq in den Mix werfen, wofür meine Implementierung ursprünglich gedacht war.
Cameron MacFarland
erweiterte meine Antwort, lassen Sie mich wissen, was Sie denken
Sam Saffron
@CameronMacFarland - können Sie erklären, warum die zweite Überprüfung auf count == size erforderlich ist? Vielen Dank.
Dugas
8

System.Interactive sorgt Buffer()für diesen Zweck. Einige schnelle Tests zeigen, dass die Leistung der von Sam ähnelt.

Dahlbyk
quelle
1
Kennen Sie die Puffersemantik? Beispiel: Wenn Sie einen Enumerator haben, der 300.000 große Zeichenfolgen ausspuckt und versucht, ihn in Blöcke mit einer Größe von 10.000 aufzuteilen, wird Ihnen dann der Speicher ausgehen?
Sam Saffron
Buffer()kehrt zurück, IEnumerable<IList<T>>also ja, Sie hätten dort wahrscheinlich ein Problem - es wird nicht wie Ihres gestreamt.
Dahlbyk
7

Hier ist eine Routine zum Aufteilen von Listen, die ich vor ein paar Monaten geschrieben habe:

public static List<List<T>> Chunk<T>(
    List<T> theList,
    int chunkSize
)
{
    List<List<T>> result = theList
        .Select((x, i) => new {
            data = x,
            indexgroup = i / chunkSize
        })
        .GroupBy(x => x.indexgroup, x => x.data)
        .Select(g => new List<T>(g))
        .ToList();

    return result;
}
Amy B.
quelle
6

Ich finde, dieser kleine Ausschnitt macht den Job ganz gut.

public static IEnumerable<List<T>> Chunked<T>(this List<T> source, int chunkSize)
{
    var offset = 0;

    while (offset < source.Count)
    {
        yield return source.GetRange(offset, Math.Min(source.Count - offset, chunkSize));
        offset += chunkSize;
    }
}
erlando
quelle
5

Was ist mit diesem?

var input = new List<string> { "a", "g", "e", "w", "p", "s", "q", "f", "x", "y", "i", "m", "c" };
var k = 3

var res = Enumerable.Range(0, (input.Count - 1) / k + 1)
                    .Select(i => input.GetRange(i * k, Math.Min(k, input.Count - i * k)))
                    .ToList();

Soweit ich weiß, ist GetRange () in Bezug auf die Anzahl der aufgenommenen Elemente linear. Das sollte also gut funktionieren.

Roman Pekar
quelle
5

Dies ist eine alte Frage, aber damit bin ich gelandet. Es listet die Aufzählung nur einmal auf, erstellt jedoch Listen für jede der Partitionen. Es leidet nicht unter unerwartetem Verhalten, wenn ToArray()es wie einige der Implementierungen aufgerufen wird:

    public static IEnumerable<IEnumerable<T>> Partition<T>(IEnumerable<T> source, int chunkSize)
    {
        if (source == null)
        {
            throw new ArgumentNullException("source");
        }

        if (chunkSize < 1)
        {
            throw new ArgumentException("Invalid chunkSize: " + chunkSize);
        }

        using (IEnumerator<T> sourceEnumerator = source.GetEnumerator())
        {
            IList<T> currentChunk = new List<T>();
            while (sourceEnumerator.MoveNext())
            {
                currentChunk.Add(sourceEnumerator.Current);
                if (currentChunk.Count == chunkSize)
                {
                    yield return currentChunk;
                    currentChunk = new List<T>();
                }
            }

            if (currentChunk.Any())
            {
                yield return currentChunk;
            }
        }
    }
aolszowka
quelle
Wäre gut, dies in eine Erweiterungsmethode umzuwandeln:public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int chunkSize)
krizzzn
+1 für deine Antwort. Ich empfehle jedoch zwei Dinge: 1. Verwenden Sie foreach anstelle von while und verwenden Sie block. 2. Übergeben Sie chunkSize im Konstruktor von List, damit die Liste ihre maximal erwartete Größe kennt.
Usman Zafar
4

Wir fanden, dass die Lösung von David B am besten funktioniert. Aber wir haben es an eine allgemeinere Lösung angepasst:

list.GroupBy(item => item.SomeProperty) 
   .Select(group => new List<T>(group)) 
   .ToArray();
mwjackson
quelle
3
Das ist schön, aber ganz anders als das, wonach der ursprüngliche Fragesteller gefragt hat.
Amy B
4

Diese folgende Lösung ist die kompakteste, die ich mir vorstellen kann: O (n).

public static IEnumerable<T[]> Chunk<T>(IEnumerable<T> source, int chunksize)
{
    var list = source as IList<T> ?? source.ToList();
    for (int start = 0; start < list.Count; start += chunksize)
    {
        T[] chunk = new T[Math.Min(chunksize, list.Count - start)];
        for (int i = 0; i < chunk.Length; i++)
            chunk[i] = list[start + i];

        yield return chunk;
    }
}
Marc-André Bertrand
quelle
4

Alter Code, aber das habe ich benutzt:

    public static IEnumerable<List<T>> InSetsOf<T>(this IEnumerable<T> source, int max)
    {
        var toReturn = new List<T>(max);
        foreach (var item in source)
        {
            toReturn.Add(item);
            if (toReturn.Count == max)
            {
                yield return toReturn;
                toReturn = new List<T>(max);
            }
        }
        if (toReturn.Any())
        {
            yield return toReturn;
        }
    }
Robert McKee
quelle
Nach dem Posten wurde mir klar, dass dies ziemlich genau der gleiche Code ist, den casperOne vor 6 Jahren gepostet hat, mit der Änderung der Verwendung von .Any () anstelle von .Count (), da ich nicht die gesamte Anzahl benötige, sondern nur wissen muss, ob es einen gibt .
Robert McKee
3

Wenn die Liste vom Typ system.collections.generic ist, können Sie die verfügbare Methode "CopyTo" verwenden, um Elemente Ihres Arrays in andere Unterarrays zu kopieren. Sie geben das Startelement und die Anzahl der zu kopierenden Elemente an.

Sie können auch 3 Klone Ihrer ursprünglichen Liste erstellen und den "RemoveRange" in jeder Liste verwenden, um die Liste auf die gewünschte Größe zu verkleinern.

Oder erstellen Sie einfach eine Hilfsmethode, um dies für Sie zu tun.

Jobo
quelle
2

Es ist eine alte Lösung, aber ich hatte einen anderen Ansatz. Ich Skipbewege mich zum gewünschten Versatz und Takeextrahiere die gewünschte Anzahl von Elementen:

public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, 
                                                   int chunkSize)
{
    if (chunkSize <= 0)
        throw new ArgumentOutOfRangeException($"{nameof(chunkSize)} should be > 0");

    var nbChunks = (int)Math.Ceiling((double)source.Count()/chunkSize);

    return Enumerable.Range(0, nbChunks)
                     .Select(chunkNb => source.Skip(chunkNb*chunkSize)
                     .Take(chunkSize));
}
Bertrand
quelle
1
Sehr ähnlich einem Ansatz, den ich verwendet habe, aber ich empfehle, dass die Quelle nicht IEnumerable ist. Wenn die Quelle beispielsweise das Ergebnis einer LINQ-Abfrage ist, würde das Überspringen / Nehmen nbChunk-Aufzählungen der Abfrage auslösen. Könnte teuer werden. Besser wäre es, IList oder ICollection als Typ für die Quelle zu verwenden. Das vermeidet das Problem insgesamt.
RB Davidson
2

Für alle, die an einer gepackten / gewarteten Lösung interessiert sind, bietet die MoreLINQ- Bibliothek die BatchErweiterungsmethode, die Ihrem gewünschten Verhalten entspricht:

IEnumerable<char> source = "Example string";
IEnumerable<IEnumerable<char>> chunksOfThreeChars = source.Batch(3);

Die BatchImplementierung ähnelt der Antwort von Cameron MacFarland , mit der Hinzufügung einer Überladung zum Transformieren des Chunks / Batch vor der Rückkehr, und funktioniert recht gut.

Kevinoid
quelle
Dies sollte die akzeptierte Antwort sein. Anstatt das Rad neu zu erfinden, sollte Morelinq verwendet werden
Otabek Kholikov
1

Verwenden der modularen Partitionierung:

public IEnumerable<IEnumerable<string>> Split(IEnumerable<string> input, int chunkSize)
{
    var chunks = (int)Math.Ceiling((double)input.Count() / (double)chunkSize);
    return Enumerable.Range(0, chunks).Select(id => input.Where(s => s.GetHashCode() % chunks == id));
}
Janosz G.
quelle
1

Ich lege nur meine zwei Cent ein. Wenn Sie die Liste "zusammenfassen" möchten (von links nach rechts visualisieren), können Sie Folgendes tun:

 public static List<List<T>> Buckets<T>(this List<T> source, int numberOfBuckets)
    {
        List<List<T>> result = new List<List<T>>();
        for (int i = 0; i < numberOfBuckets; i++)
        {
            result.Add(new List<T>());
        }

        int count = 0;
        while (count < source.Count())
        {
            var mod = count % numberOfBuckets;
            result[mod].Add(source[count]);
            count++;
        }
        return result;
    }
mattylantz
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
IMHO am meisten porper Antwort.
Stanislav Berkov
1
public static List<List<T>> GetSplitItemsList<T>(List<T> originalItemsList, short number)
    {
        var listGroup = new List<List<T>>();
        int j = number;
        for (int i = 0; i < originalItemsList.Count; i += number)
        {
            var cList = originalItemsList.Take(j).Skip(i).ToList();
            j += number;
            listGroup.Add(cList);
        }
        return listGroup;
    }
Joy Zhu
quelle
0

Ich nahm die primäre Antwort und machte sie zu einem IOC-Container, um zu bestimmen, wo aufgeteilt werden soll. ( Für wen, der wirklich nur 3 Elemente aufteilen möchte, wenn er diesen Beitrag liest, während er nach einer Antwort sucht? )

Diese Methode ermöglicht es, nach Bedarf auf jeden Elementtyp aufzuteilen.

public static List<List<T>> SplitOn<T>(List<T> main, Func<T, bool> splitOn)
{
    int groupIndex = 0;

    return main.Select( item => new 
                             { 
                               Group = (splitOn.Invoke(item) ? ++groupIndex : groupIndex), 
                               Value = item 
                             })
                .GroupBy( it2 => it2.Group)
                .Select(x => x.Select(v => v.Value).ToList())
                .ToList();
}

Für das OP wäre der Code also

var it = new List<string>()
                       { "a", "g", "e", "w", "p", "s", "q", "f", "x", "y", "i", "m", "c" };

int index = 0; 
var result = SplitOn(it, (itm) => (index++ % 3) == 0 );
ΩmegaMan
quelle
0

So performatisch wie der Ansatz von Sam Saffron .

public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size), "Size must be greater than zero.");

    return BatchImpl(source, size).TakeWhile(x => x.Any());
}

static IEnumerable<IEnumerable<T>> BatchImpl<T>(this IEnumerable<T> source, int size)
{
    var values = new List<T>();
    var group = 1;
    var disposed = false;
    var e = source.GetEnumerator();

    try
    {
        while (!disposed)
        {
            yield return GetBatch(e, values, group, size, () => { e.Dispose(); disposed = true; });
            group++;
        }
    }
    finally
    {
        if (!disposed)
            e.Dispose();
    }
}

static IEnumerable<T> GetBatch<T>(IEnumerator<T> e, List<T> values, int group, int size, Action dispose)
{
    var min = (group - 1) * size + 1;
    var max = group * size;
    var hasValue = false;

    while (values.Count < min && e.MoveNext())
    {
        values.Add(e.Current);
    }

    for (var i = min; i <= max; i++)
    {
        if (i <= values.Count)
        {
            hasValue = true;
        }
        else if (hasValue = e.MoveNext())
        {
            values.Add(e.Current);
        }
        else
        {
            dispose();
        }

        if (hasValue)
            yield return values[i - 1];
        else
            yield break;
    }
}

}}

Leandromoh
quelle
0

Kann mit unendlichen Generatoren arbeiten:

a.Zip(a.Skip(1), (x, y) => Enumerable.Repeat(x, 1).Concat(Enumerable.Repeat(y, 1)))
 .Zip(a.Skip(2), (xy, z) => xy.Concat(Enumerable.Repeat(z, 1)))
 .Where((x, i) => i % 3 == 0)

Demo-Code: https://ideone.com/GKmL7M

using System;
using System.Collections.Generic;
using System.Linq;

public class Test
{
  private static void DoIt(IEnumerable<int> a)
  {
    Console.WriteLine(String.Join(" ", a));

    foreach (var x in a.Zip(a.Skip(1), (x, y) => Enumerable.Repeat(x, 1).Concat(Enumerable.Repeat(y, 1))).Zip(a.Skip(2), (xy, z) => xy.Concat(Enumerable.Repeat(z, 1))).Where((x, i) => i % 3 == 0))
      Console.WriteLine(String.Join(" ", x));

    Console.WriteLine();
  }

  public static void Main()
  {
    DoIt(new int[] {1});
    DoIt(new int[] {1, 2});
    DoIt(new int[] {1, 2, 3});
    DoIt(new int[] {1, 2, 3, 4});
    DoIt(new int[] {1, 2, 3, 4, 5});
    DoIt(new int[] {1, 2, 3, 4, 5, 6});
  }
}
1

1 2

1 2 3
1 2 3

1 2 3 4
1 2 3

1 2 3 4 5
1 2 3

1 2 3 4 5 6
1 2 3
4 5 6

Aber eigentlich würde ich lieber eine entsprechende Methode ohne linq schreiben.

Qwertiy
quelle
0

Schau dir das an! Ich habe eine Liste von Elementen mit einem Sequenzzähler und einem Datum. Bei jedem Neustart der Sequenz möchte ich eine neue Liste erstellen.

Ex. Liste der Nachrichten.

 List<dynamic> messages = new List<dynamic>
        {
            new { FcntUp = 101, CommTimestamp = "2019-01-01 00:00:01" },
            new { FcntUp = 102, CommTimestamp = "2019-01-01 00:00:02" },
            new { FcntUp = 103, CommTimestamp = "2019-01-01 00:00:03" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:04" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:05" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:06" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:07" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:08" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:09" }
        };

Ich möchte die Liste beim Neustart des Zählers in separate Listen aufteilen. Hier ist der Code:

var arraylist = new List<List<dynamic>>();

        List<dynamic> messages = new List<dynamic>
        {
            new { FcntUp = 101, CommTimestamp = "2019-01-01 00:00:01" },
            new { FcntUp = 102, CommTimestamp = "2019-01-01 00:00:02" },
            new { FcntUp = 103, CommTimestamp = "2019-01-01 00:00:03" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:04" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:05" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:06" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:07" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:08" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:09" }
        };

        //group by FcntUp and CommTimestamp
        var query = messages.GroupBy(x => new { x.FcntUp, x.CommTimestamp });

        //declare the current item
        dynamic currentItem = null;

        //declare the list of ranges
        List<dynamic> range = null;

        //loop through the sorted list
        foreach (var item in query)
        {
            //check if start of new range
            if (currentItem == null || item.Key.FcntUp < currentItem.Key.FcntUp)
            {
                //create a new list if the FcntUp starts on a new range
                range = new List<dynamic>();

                //add the list to the parent list
                arraylist.Add(range);
            }

            //add the item to the sublist
            range.Add(item);

            //set the current item
            currentItem = item;
        }
Claes-Philip Staiger
quelle
-1

Um meine zwei Cent einzufügen ...

Durch die Verwendung des Listentyps für die Quelle, die aufgeteilt werden soll, habe ich eine andere sehr kompakte Lösung gefunden:

public static IEnumerable<IEnumerable<TSource>> Chunk<TSource>(this IEnumerable<TSource> source, int chunkSize)
{
    // copy the source into a list
    var chunkList = source.ToList();

    // return chunks of 'chunkSize' items
    while (chunkList.Count > chunkSize)
    {
        yield return chunkList.GetRange(0, chunkSize);
        chunkList.RemoveRange(0, chunkSize);
    }

    // return the rest
    yield return chunkList;
}
Patrick
quelle