Dividi la lista in Sublist con LINQ


377

Esiste un modo per separare a List<SomeObject>in diversi elenchi separati di SomeObject, usando l'indice degli articoli come delimitatore di ogni divisione?

Vorrei esemplificare:

Ho un List<SomeObject>e ho bisogno di un List<List<SomeObject>>o List<SomeObject>[], in modo che ciascuno di questi elenchi risultanti conterrà un gruppo di 3 elementi dell'elenco originale (in sequenza).

per esempio.:

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

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

Avrei anche bisogno delle dimensioni delle liste risultanti per essere un parametro di questa funzione.

Risposte:


378

Prova il seguente codice.

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

L'idea è di raggruppare prima gli elementi per indici. La divisione per tre ha l'effetto di raggrupparli in gruppi di 3. Quindi convertire ciascun gruppo in un elenco e il IEnumerabledi Lista in Listdi Lists


21
GroupBy fa un ordinamento implicito. Questo può uccidere le prestazioni. Ciò di cui abbiamo bisogno è una sorta di inverso di SelectMany.
yfeldblum,

5
@Justice, GroupBy potrebbe essere implementato tramite hashing. Come fai a sapere che l'implementazione di GroupBy "può uccidere le prestazioni"?
Amy B,

5
GroupBy non restituisce nulla fino a quando non viene elencato tutti gli elementi. Ecco perché è lento. Gli elenchi richiesti da OP sono contigui, quindi un metodo migliore potrebbe produrre il primo elenco secondario [a,g,e]prima di elencare qualsiasi altro elenco originale.
Colonnello Panic,

9
Prendi l'esempio estremo di un infinito IEnumerable. GroupBy(x=>f(x)).First()non produrrà mai un gruppo. OP ha chiesto degli elenchi, ma se scriviamo per lavorare con IEnumerable, effettuando una sola iterazione, ne trarremo vantaggio.
Colonnello Panic,

8
L'ordine @Nick non è preservato a modo tuo però. È comunque una buona cosa sapere ma li raggrupperesti in (0,3,6,9, ...), (1,4,7,10, ...), (2,5,8 , 11, ...). Se l'ordine non ha importanza, allora va bene, ma in questo caso sembra che sia importante.
Reafexus,

325

Questa domanda è un po 'vecchia, ma l'ho appena scritta e penso che sia un po' più elegante delle altre soluzioni proposte:

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

14
Adoro questa soluzione. Consiglio di aggiungere questo controllo di if (chunksize <= 0) throw new ArgumentException("Chunk size must be greater than zero.", "chunksize");
integrità

10
Mi piace questo, ma non è super efficiente
Sam Saffron

51
Mi piace questo, ma l'efficienza nel tempo lo è O(n²). Puoi scorrere l'elenco e ottenere un O(n)po 'di tempo.
hIpPy

8
@hIpPy, com'è n ^ 2? Mi sembra lineare
Vivek Maharajh,

13
@vivekmaharajh sourceviene sostituito da un involucro IEnumerableogni volta. Quindi prendere elementi da sourcepassa attraverso strati di Skips
Lasse Espeholt

99

In generale l'approccio suggerito da CaseyB funziona bene, infatti se si passa in un List<T>è difficile criticare, forse lo cambierei 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;
   }
}

Ciò eviterà enormi catene di chiamate. Tuttavia, questo approccio ha un difetto generale. Si materializza due enumerazioni per blocco, per evidenziare il problema provare a eseguire:

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

Per ovviare a questo, possiamo provare l' approccio di Cameron , che supera a pieni voti il ​​test di cui sopra, percorrendo l'enumerazione solo una volta.

Il problema è che ha un difetto diverso, materializza ogni elemento in ogni blocco, il problema con quell'approccio è che hai poca memoria.

Per illustrare che provare a eseguire:

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

Infine, qualsiasi implementazione dovrebbe essere in grado di gestire iterazioni non ordinate di blocchi, ad esempio:

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

Molte soluzioni altamente ottimali come la mia prima revisione di questa risposta sono fallite lì. Lo stesso problema si può vedere nella risposta ottimizzata di casperOne .

Per risolvere tutti questi problemi è possibile utilizzare quanto segue:

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


        }

    }
}

Esiste anche una serie di ottimizzazioni che è possibile introdurre per l'iterazione non ordinata di blocchi, che qui non rientra nell'ambito.

Quale metodo scegliere? Dipende totalmente dal problema che stai cercando di risolvere. Se non ti preoccupi del primo difetto, la semplice risposta è incredibilmente attraente.

Nota come con la maggior parte dei metodi, questo non è sicuro per il multi threading, le cose possono diventare strane se desideri rendere sicuro il thread che dovresti modificare EnumeratorWrapper.


Il bug sarebbe Enumerable.Range (0, 100) .Chunk (3) .Reverse (). ToArray () errato o Enumerable.Range (0, 100) .ToArray (). Chunk (3) .Reverse () .ToArray () che genera un'eccezione?
Cameron MacFarland,

@SamSaffron Ho aggiornato la mia risposta e semplificato enormemente il codice per quello che ritengo sia il caso d'uso prominente (e riconosco le avvertenze).
casper:

Che ne dici di chuncking IQueryable <>? La mia ipotesi è che un approccio Take / Skip sarebbe ottimale se vogliamo delegare un massimo di operazioni al provider
Guillaume86,

@ Guillaume86 Sono d'accordo, se hai un IList o IQueryable puoi prendere tutti i tipi di scorciatoie che lo renderebbero molto più veloce (Linq lo fa internamente per tutti i tipi di altri metodi)
Sam Saffron,

1
Questa è di gran lunga la migliore risposta per l'efficienza. Sto riscontrando un problema utilizzando SqlBulkCopy con un IEnumerable che esegue processi aggiuntivi su ogni colonna, quindi deve essere eseguito in modo efficiente con un solo passaggio. Questo mi permetterà di suddividere l'IEnumerable in blocchi di dimensioni gestibili. (Per quelli che si chiedono, ho abilitato la modalità di streaming di SqlBulkCopy, che sembra essere rotta).
Brain2000,

64

Si potrebbe utilizzare una serie di query che utilizzano Takee Skip, ma che sarebbe aggiungere troppi iterazioni sulla lista originale, credo.

Piuttosto, penso che dovresti creare un iteratore tuo, in questo modo:

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

È quindi possibile chiamare questo ed è abilitato LINQ in modo da poter eseguire altre operazioni sulle sequenze risultanti.


Alla luce della risposta di Sam , ho sentito che c'era un modo più semplice per farlo senza:

  • Scorrendo di nuovo l'elenco (cosa che non avevo fatto inizialmente)
  • Materializzare gli oggetti in gruppi prima di rilasciare il blocco (per grossi pezzi di oggetti, ci sarebbero problemi di memoria)
  • Tutto il codice pubblicato da Sam

Detto questo, ecco un altro passaggio, che ho codificato in un metodo di estensione per IEnumerable<T>chiamare 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);
}

Niente di sorprendente lassù, solo il controllo degli errori di base.

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

Fondamentalmente, ottiene il IEnumerator<T> e scorre manualmente ogni elemento. Verifica se ci sono elementi da elencare al momento. Dopo che ogni blocco è stato elencato, se non ci sono elementi rimasti, scoppia.

Una volta rilevato che ci sono elementi nella sequenza, delega la responsabilità dell'implementazione interna IEnumerable<T>a 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());
}

Poiché MoveNextè già stato chiamato al IEnumerator<T>passato ChunkSequence, restituisce l'oggetto restituito da Currente quindi incrementa il conteggio, assicurandosi di non restituire più degli chunkSizearticoli e spostandosi all'elemento successivo nella sequenza dopo ogni iterazione (ma in cortocircuito se il numero di gli articoli prodotti superano le dimensioni del pezzo).

Se non sono rimasti elementi, il InternalChunkmetodo eseguirà un altro passaggio nel loop esterno, ma quando MoveNextviene chiamato una seconda volta, restituirà comunque false, come indicato nella documentazione (sottolineatura mia):

Se MoveNext supera la fine della raccolta, l'enumeratore viene posizionato dopo l'ultimo elemento della raccolta e MoveNext restituisce false. Quando l'enumeratore si trova in questa posizione, anche le chiamate successive a MoveNext restituiscono false fino a quando non viene chiamato Reset.

A questo punto, il loop si interromperà e la sequenza di sequenze terminerà.

Questo è un semplice 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();
    }
}

Produzione:

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

Una nota importante, questo non funzionerà se non si esaurisce l'intera sequenza figlio o si interrompe in qualsiasi punto della sequenza padre. Questo è un avvertimento importante, ma se il tuo caso d'uso è che ne consumerai tutti elemento della sequenza di sequenze, allora funzionerà per te.

Inoltre, farà cose strane se giochi con l'ordine, proprio come ha fatto Sam a un certo punto .


Penso che questa sia la soluzione migliore ... l'unico problema è che l'elenco non ha Lunghezza ... ha Conteggio. Ma è facile da cambiare. Possiamo renderlo migliore nemmeno costruendo elenchi ma restituendo ienumerabili che contengono riferimenti all'elenco principale con una combinazione offset / lunghezza. Quindi, se la dimensione del gruppo è grande, non sprechiamo memoria. Commenta se vuoi che lo scriva.
Amir,

@Amir mi piacerebbe vederlo scritto
samandmoore

Questo è bello e veloce - Cameron ne ha pubblicato uno molto simile anche dopo il tuo, l'unico avvertimento è che bufferizza blocchi, questo può portare a memoria insufficiente se blocchi e dimensioni degli oggetti sono grandi. Vedi la mia risposta per un'alternativa, anche se molto più pelosa, risposta.
Sam Saffron,

@SamSaffron Sì, se hai un gran numero di elementi nel List<T>, ovviamente avrai problemi di memoria a causa del buffering. Col senno di poi, avrei dovuto annotarlo nella risposta, ma sembrava che al momento l'attenzione fosse concentrata su troppe iterazioni. Detto questo, la tua soluzione è davvero più pelosa. Non l'ho provato, ma ora mi chiedo se c'è una soluzione meno pelosa.
casper:

Sì elenco che esplode (in realtà dapper ha un buffer: falsa opzione solo per questo caso d'uso)
Sam Saffron

48

Ok, ecco la mia opinione su di esso:

  • completamente pigro: funziona su infiniti enumerabili
  • nessuna copia / buffering intermedio
  • O (n) tempo di esecuzione
  • funziona anche quando le sequenze interne vengono consumate solo parzialmente

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

Esempio di utilizzo

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

spiegazioni

Il codice funziona annidando due yielditeratori basati.

L'iteratore esterno deve tenere traccia di quanti elementi sono stati effettivamente consumati dall'iteratore interno (blocco). Questo viene fatto chiudendo remainingcon innerMoveNext(). Gli elementi non consumati di un blocco vengono scartati prima che il blocco successivo venga ceduto dall'iteratore esterno. Ciò è necessario perché altrimenti si ottengono risultati incoerenti, quando gli enumerabili interni non vengono (completamente) consumati (ad esempio c3.Count()restituirebbero 6).

Nota: la risposta è stata aggiornata per ovviare alle carenze evidenziate da @aolszowka.


2
Molto bella. La mia soluzione "corretta" era molto più complicata di così. Questa è la risposta numero 1 IMHO.
CaseyB,

Questo soffre di un comportamento imprevisto (dal punto di vista dell'API) quando viene chiamato ToArray (), inoltre non è thread-safe.
Aolszowka,

@aolszowka: potresti per favore elaborare?
3dGrabber

@ 3dGrabber Forse è stato come ho ripensato il tuo codice (mi dispiace che sia un po 'troppo lungo per essere passato qui, fondamentalmente invece di un metodo di estensione che ho passato in SourceEnumerator). Il caso di test che ho usato era qualcosa in tal senso: int [] arrayToSort = new int [] {9, 7, 2, 6, 3, 4, 8, 5, 1, 10, 11, 12, 13}; var source = Chunkify <int> (arrayToSort, 3) .ToArray (); Il risultato è Sorgente che indica che c'erano 13 blocchi (il numero di elementi). Questo aveva senso per me come se non avessi interrogato le enumerazioni interne che l'enumeratore non fosse incrementato.
Aolszowka,

1
@aolszowka: punti molto validi. Ho aggiunto un avviso e una sezione di utilizzo. Il codice presuppone che si esegua l'iterazione sull'enumerabile interno. Con la tua soluzione, però, perdi la pigrizia. Penso che dovrebbe essere possibile ottenere il meglio da entrambi i mondi con un IEnumerator personalizzato e con memorizzazione nella cache. Se trovo una soluzione, la
posterò

18

completamente pigro, senza contare o copiare:

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

Questa soluzione è così elegante che mi dispiace di non poter votare questa risposta più di una volta.
Segna il

3
Non penso che questo fallirebbe esattamente. Ma potrebbe sicuramente avere un comportamento strano. Se tu avessi 100 articoli e li dividessi in lotti di 10, e hai elencato tutti i lotti senza enumerare gli articoli di quei lotti,
finiresti

1
Come accennato @CaseyB, questo soffre della stessa mancanza 3dGrabber affrontati qui stackoverflow.com/a/20953521/1037948 , ma l'uomo è in fretta!
drzaus,

1
Questa è una bella soluzione. Fa esattamente quello che promette.
Rod Hartzell,

Di gran lunga la soluzione più elegante e precisa. L'unica cosa è che dovresti aggiungere un controllo per i numeri negativi e sostituire ArgumentNullException con ArgumentException
Romain Vergnory

13

Penso che il seguente suggerimento sarebbe il più veloce. Sto sacrificando la pigrizia della fonte Enumerable per la capacità di usare Array.Copy e conoscendo in anticipo la lunghezza di ciascuna delle mie liste.

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

Non solo più veloce, gestisce anche correttamente ulteriori operazioni enumerabili sul risultato, ad esempio items.Chunk (5) .Reverse (). SelectMany (x => x)
troppo

9

Possiamo migliorare la soluzione di @ JaredPar per fare una vera valutazione pigra. Usiamo un GroupAdjacentBymetodo che produce gruppi di elementi consecutivi con la stessa chiave:

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

Poiché i gruppi vengono prodotti uno a uno, questa soluzione funziona in modo efficiente con sequenze lunghe o infinite.


8

Ho scritto un metodo di estensione Clump diversi anni fa. Funziona alla grande ed è l'implementazione più veloce qui. : 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;
        }
    }
}

dovrebbe funzionare ma sta tamponando il 100% dei pezzi, stavo cercando di evitarlo ... ma risulta incredibilmente peloso.
Sam Saffron,

@SamSaffron Yep. Soprattutto se si gettano cose come plinq nel mix, che è stato originariamente per la mia implementazione.
Cameron MacFarland,

ho ampliato la mia risposta, fammi sapere cosa ne pensi
Sam Saffron,

@CameronMacFarland - puoi spiegare perché è necessario il secondo controllo per count == size? Grazie.
dugas,

8

System.Interactive prevede Buffer()questo scopo. Alcuni test rapidi mostrano che le prestazioni sono simili alla soluzione di Sam.


1
conosci la semantica del buffering? Ad esempio: se si dispone di un enumeratore che sputa stringhe grandi 300k e si tenta di dividerlo in blocchi di dimensioni di 10.000, si otterrà memoria insufficiente?
Sam Saffron,

Buffer()ritorna IEnumerable<IList<T>>quindi sì, probabilmente avresti un problema lì - non scorre come il tuo.
dahlbyk,

7

Ecco un elenco di routine che ho scritto un paio di mesi fa:

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

6

Trovo che questo piccolo frammento funzioni abbastanza bene.

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

5

Che dire di questa?

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

Per quanto ne so, GetRange () è lineare in termini di numero di elementi presi. Quindi questo dovrebbe funzionare bene.


5

Questa è una vecchia domanda ma questo è quello che ho finito; enumera l'enumerabile una sola volta, ma crea elenchi per ciascuna delle partizioni. Non soffre di comportamenti imprevisti quando ToArray()viene chiamato come fanno alcune delle implementazioni:

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

Sarebbe bene convertirlo in un metodo di estensione:public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int chunkSize)
krizzzn,

+1 per la tua risposta. Comunque ti consiglio due cose 1. usa foreach invece di while e usa block. 2. Passa chunkSize nel costruttore di List in modo che l'elenco conosca la dimensione massima prevista.
Usman Zafar,

4

Abbiamo scoperto che la soluzione di David B ha funzionato al meglio. Ma l'abbiamo adattato a una soluzione più generale:

list.GroupBy(item => item.SomeProperty) 
   .Select(group => new List<T>(group)) 
   .ToArray();

3
Questo è carino, ma abbastanza diverso da quello che chiedeva il richiedente originale.
Amy B,

4

Questa soluzione che segue è la più compatta che ho potuto trovare, ovvero 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;
    }
}

4

Vecchio codice, ma questo è quello che ho usato:

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

Dopo la pubblicazione, mi sono reso conto che questo è praticamente lo stesso codice casperOne pubblicato 6 anni fa con il cambio di utilizzo di .Any () invece di .Count () poiché non ho bisogno dell'intero conteggio, ho solo bisogno di sapere se esiste .
Robert McKee,

3

Se l'elenco è di tipo system.collections.generic è possibile utilizzare il metodo "CopyTo" disponibile per copiare elementi dell'array in altri array secondari. Si specifica l'elemento iniziale e il numero di elementi da copiare.

È inoltre possibile creare 3 cloni dell'elenco originale e utilizzare "RemoveRange" su ciascun elenco per ridurre l'elenco alle dimensioni desiderate.

O semplicemente crea un metodo di supporto per farlo per te.


2

È una vecchia soluzione ma ho avuto un approccio diverso. Uso Skipper passare all'offset desiderato ed Takeestrarre il numero desiderato di elementi:

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

1
Molto simile a un approccio che ho usato, ma raccomando che la fonte non sia IEnumerable. Ad esempio, se l'origine è il risultato di una query LINQ, Skip / Take attiverà le enumerazioni nbChunk della query. Potrebbe essere costoso. Meglio sarebbe usare IList o ICollection come tipo di sorgente. Ciò evita del tutto il problema.
RB Davidson,

2

Per chiunque sia interessato a una soluzione pacchettizzata / gestita, la libreria MoreLINQ fornisce il Batchmetodo di estensione che corrisponde al comportamento richiesto:

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

L' Batchimplementazione è simile alla risposta di Cameron MacFarland , con l'aggiunta di un sovraccarico per la trasformazione del blocco / batch prima del ritorno, e si comporta abbastanza bene.


questa dovrebbe essere la risposta accettata. Invece di reinventare la ruota, dovrebbe essere usato il
morelinq

1

Utilizzando il partizionamento modulare:

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

1

Sto solo mettendo i miei due centesimi. Se si desidera "seccare" l'elenco (visualizzare da sinistra a destra), è possibile effettuare le seguenti operazioni:

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

1

Un altro modo è utilizzare l' operatore Buffer Rx

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

La risposta più delicata di IMHO.
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;
    }

0

Ho preso la risposta principale e l'ho fatta diventare un contenitore IOC per determinare dove dividere. ( Per chi sta davvero cercando di dividere solo 3 elementi, leggendo questo post mentre cerchi una risposta? )

Questo metodo consente di dividere su qualsiasi tipo di elemento, se necessario.

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

Quindi per l'OP il codice sarebbe

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

0

Così performante come l' approccio di 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;
    }
}

}


0

Può funzionare con generatori infiniti:

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)

Codice demo: 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

Ma in realtà preferirei scrivere il metodo corrispondente senza linq.


0

Controllalo! Ho un elenco di elementi con un contatore di sequenza e una data. Per ogni riavvio della sequenza, voglio creare un nuovo elenco.

Ex. elenco di messaggi.

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

Voglio dividere l'elenco in elenchi separati al riavvio del contatore. Ecco il codice:

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

-1

Per inserire i miei due centesimi ...

Usando il tipo di elenco per la sorgente da dividere, ho trovato un'altra soluzione molto compatta:

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;
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.