475 Stimmen

Liste in Unterlisten aufteilen mit LINQ

Gibt es eine Möglichkeit, wie ich eine List<SomeObject> in mehrere separate Listen von SomeObject und dabei den Artikelindex als Trennzeichen für jeden Split?

Lassen Sie mich ein Beispiel geben:

Ich habe eine List<SomeObject> und ich brauche eine List<List<SomeObject>> o List<SomeObject>[] , so dass jede dieser resultierenden Listen eine Gruppe von 3 Elementen der ursprünglichen Liste (in der Reihenfolge) enthält.

z. B.:

  • Ursprüngliche Liste: [a, g, e, w, p, s, q, f, x, y, i, m, c]

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

Außerdem muss die Größe der resultierenden Liste ein Parameter dieser Funktion sein.

449voto

JaredPar Punkte 699699

Probieren Sie den folgenden Code aus.

public static List<List<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 zunächst nach Indizes zu gruppieren. Die Division durch drei bewirkt, dass sie in 3er-Gruppen gruppiert werden. Dann wird jede Gruppe in eine Liste umgewandelt und die IEnumerable de List zu einer List de List s

401voto

CaseyB Punkte 4049

Das habe ich gerade geschrieben, und ich denke, es ist ein wenig 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);
    }
}

122voto

Sam Saffron Punkte 124121

Im Allgemeinen wird der Ansatz von CaseyB funktioniert gut, und zwar wenn Sie eine List<T> es ist schwer, daran etwas auszusetzen, vielleicht würde ich es ä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;
   }
}

Dadurch werden massive Anrufketten vermieden. Dennoch hat dieser Ansatz einen generellen Makel. Es materialisiert zwei Aufzählungen pro Chunk, um das Problem hervorzuheben versuchen zu laufen:

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 versuchen Cameron's Ansatz, der den obigen Test mit Bravour besteht, da er die Aufzählung nur einmal durchläuft.

Das Problem ist, dass es eine andere Schwachstelle hat, es materialisiert jedes Element in jedem Chunk, das Problem mit diesem Ansatz ist, dass Sie viel Speicher laufen.

Um dies zu veranschaulichen, versuchen Sie es mit einem Lauf:

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, z. B. die Iteration von Chunks außerhalb der Reihenfolge zu behandeln:

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

Viele sehr optimale Lösungen wie meine erste Revision dieser Antwort ist dort gescheitert. Das gleiche Problem ist zu sehen in casperOne ist optimiert Antwort.

Um all diese Probleme zu lösen, können Sie die folgenden Maßnahmen ergreifen:

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 man für die Iteration von Chunks außerhalb der Reihenfolge einführen könnte, was hier aber den Rahmen sprengen würde.

Für welche Methode sollten Sie sich entscheiden? Das hängt ganz von dem Problem ab, das Sie zu lösen versuchen. Wenn Sie sich nicht um den ersten Fehler kümmern, ist die einfache Antwort unglaublich verlockend.

Hinweis wie bei den meisten Methoden, ist dies nicht sicher für Multi-Threading, Dinge können seltsam werden, wenn Sie es Thread sicher machen möchten, müssten Sie ändern EnumeratorWrapper .

77voto

Majid Shahabfar Punkte 2166

Aktualisierung von .NET 6.0

.NET 6.0 fügte eine neue native Stückchen Methode in den System.Linq-Namensraum:

public static System.Collections.Generic.IEnumerable<TSource[]> Chunk<TSource> (
   this System.Collections.Generic.IEnumerable<TSource> source, int size);

Mit dieser neuen Methode haben alle Chunks außer dem letzten die Größe size . Das letzte Stück enthält die verbleibenden Elemente und kann eine geringere Größe haben.

Hier ist ein Beispiel:

var list = Enumerable.Range(1, 100);
var chunkSize = 10;

foreach(var chunk in list.Chunk(chunkSize)) //Returns a chunk with the correct size. 
{
    Parallel.ForEach(chunk, (item) =>
    {
        //Do something Parallel here. 
        Console.WriteLine(item);
    });
}

Sie denken jetzt wahrscheinlich, warum nicht "Skip and Take" verwenden? Das ist richtig, aber ich denke, das ist ein bisschen prägnanter und macht die Dinge ein bisschen lesbarer.

74voto

casperOne Punkte 72238

Sie könnte verwenden eine Reihe von Abfragen, die Take y Skip aber das würde zu viele Iterationen auf der ursprünglichen Liste hinzufügen, glaube ich.

Ich denke, Sie sollten stattdessen einen eigenen Iterator erstellen, etwa so:

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 diese dann aufrufen und sie ist LINQ-fähig, so dass Sie andere Operationen mit den resultierenden Sequenzen durchführen können.


In Anbetracht der Sams Antwort Ich hatte das Gefühl, dass es einen einfacheren Weg gibt, dies zu tun, ohne:

  • Erneutes Durchgehen der Liste (was ich ursprünglich nicht getan habe)
  • Materialisierung der Elemente in Gruppen vor der Freigabe des Chunks (bei großen Chunks von Elementen gäbe es Speicherprobleme)
  • Der gesamte Code, den Sam veröffentlicht hat

Abgesehen davon gibt es noch einen weiteren Durchgang, den ich in einer Erweiterungsmethode kodifiziert habe 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(nameof(source));
    if (chunkSize <= 0) throw new ArgumentOutOfRangeException(nameof(chunkSize),
        "The chunkSize parameter must be a positive value.");

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

Hier gibt es nichts Überraschendes, nur eine einfache 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);
}

Im Grunde wird die IEnumerator<T> und durchläuft jedes Element manuell. Es wird geprüft, ob es derzeit Elemente gibt, die aufgezählt werden müssen. Wenn nach der Aufzählung der einzelnen Chunks keine Elemente mehr übrig sind, wird abgebrochen.

Sobald er feststellt, dass es Elemente in der Sequenz gibt, delegiert er die Verantwortung für die inneren IEnumerable<T> Umsetzung bis 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());
}

Seit MoveNext wurde bereits auf der IEnumerator<T> übergeben an ChunkSequence ergibt sich das Element, das von Current und erhöht dann die Anzahl, wobei darauf geachtet wird, dass nie mehr als chunkSize Elemente und geht nach jeder Iteration zum nächsten Element in der Sequenz über (wird jedoch kurzgeschlossen, wenn die Anzahl der ermittelten Elemente die Größe des Chunks übersteigt).

Wenn keine Einträge mehr vorhanden sind, dann wird die InternalChunk Methode einen weiteren Durchlauf in der äußeren Schleife, aber wenn MoveNext ein zweites Mal aufgerufen wird, wird immer noch false zurückgegeben, wie in der Dokumentation beschrieben (Hervorhebung von mir):

Wenn MoveNext das Ende der Sammlung passiert, wird der Enumerator nach dem letzten Element der Sammlung positioniert und MoveNext gibt false zurück. Befindet sich der Zähler an dieser Position, werden die nachfolgenden Aufrufe von MoveNext ebenfalls false zurück, bis Reset aufgerufen wird.

An diesem Punkt bricht die Schleife ab, und die Sequenz 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 wird no funktionieren, wenn Sie nicht die gesamte untergeordnete Sequenz entleeren oder an einem beliebigen Punkt der übergeordneten Sequenz unterbrechen. Dies ist ein wichtiger Vorbehalt, aber wenn Ihr Anwendungsfall darin besteht, dass Sie cada Element der Sequenz von Sequenzen, dann wird dies für Sie funktionieren.

Außerdem macht es seltsame Dinge, wenn Sie mit der Reihenfolge spielen, genau wie Sam's hat irgendwann einmal .

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X