Crea batch in linq


104

Qualcuno può suggerire un modo per creare batch di una certa dimensione in linq?

Idealmente voglio essere in grado di eseguire operazioni in blocchi di una certa quantità configurabile.

Risposte:


116

Non è necessario scrivere alcun codice. Usa il metodo MoreLINQ Batch, che raggruppa la sequenza di origine in bucket dimensionati (MoreLINQ è disponibile come pacchetto NuGet che puoi installare):

int size = 10;
var batches = sequence.Batch(size);

Che è implementato come:

public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
                  this IEnumerable<TSource> source, int size)
{
    TSource[] bucket = null;
    var count = 0;

    foreach (var item in source)
    {
        if (bucket == null)
            bucket = new TSource[size];

        bucket[count++] = item;
        if (count != size)
            continue;

        yield return bucket;

        bucket = null;
        count = 0;
    }

    if (bucket != null && count > 0)
        yield return bucket.Take(count).ToArray();
}

3
4 byte per articolo si comportano in modo terribile ? Hai dei test che mostrano cosa significa terribilmente ? Se stai caricando milioni di elementi in memoria, non lo farei. Usa paging lato server
Sergey Berezovskiy

4
Non voglio offenderti, ma ci sono soluzioni più semplici che non si accumulano affatto. Inoltre questo assegnerà spazio anche ad elementi inesistenti:Batch(new int[] { 1, 2 }, 1000000)
Nick Whaley

7
@ NickWhaley beh, sono d'accordo con te sul fatto che verrà assegnato spazio aggiuntivo, ma nella vita reale di solito hai una situazione esattamente opposta - elenco di 1000 articoli che dovrebbero andare in lotti di 50 :)
Sergey Berezovskiy

1
Sì, la situazione di solito dovrebbe essere il contrario, ma nella vita reale, questi potrebbero essere input dell'utente.
Nick Whaley

8
Questa è una soluzione perfetta. Nella vita reale: convalidi l'input dell'utente, tratta i batch come intere raccolte di elementi (che accumula comunque gli elementi) e spesso elabora i batch in parallelo (il che non è supportato dall'approccio iteratore e sarà una brutta sorpresa a meno che tu non conosca il dettagli di implementazione).
Michael Petito

90
public static class MyExtensions
{
    public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> items,
                                                       int maxItems)
    {
        return items.Select((item, inx) => new { item, inx })
                    .GroupBy(x => x.inx / maxItems)
                    .Select(g => g.Select(x => x.item));
    }
}

e l'utilizzo sarebbe:

List<int> list = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

foreach(var batch in list.Batch(3))
{
    Console.WriteLine(String.Join(",",batch));
}

PRODUZIONE:

0,1,2
3,4,5
6,7,8
9

Ha funzionato perfettamente per me
FunMatters

16
Una volta GroupByavviata l'enumerazione, non è necessario enumerare completamente la sua fonte? Ciò perde la valutazione pigra della fonte e quindi, in alcuni casi, tutti i vantaggi del batch!
ErikE

1
Wow, grazie, mi hai salvato dalla follia. Funziona molto bene
Riaan de Lange

3
Come menzionato da @ErikE, questo metodo enumera completamente la sua fonte, quindi anche se sembra carino, sconfigge lo scopo della valutazione pigra / pipelining
lasseschou

1
Fallo: è del tutto appropriato quando devi suddividere un blocco di cose esistente in lotti più piccoli di cose per un'elaborazione performante. L'alternativa è un ciclo di ricerca grossolano in cui si scompongono manualmente i batch e si continua a passare attraverso l'intera sorgente.
StingyJack

31

Se inizi con sequencedefinito come un IEnumerable<T>e sai che può essere enumerato in modo sicuro più volte (ad esempio perché è un array o un elenco), puoi semplicemente utilizzare questo semplice modello per elaborare gli elementi in batch:

while (sequence.Any())
{
    var batch = sequence.Take(10);
    sequence = sequence.Skip(10);

    // do whatever you need to do with each batch here
}

2
Modo carino e semplice per il batch senza molto codice o la necessità di una libreria esterna
DevHawk

5
@DevHawk: lo è. Si noti, tuttavia, che le prestazioni ne risentiranno in modo esponenziale su raccolte di grandi dimensioni (r).
RobIII

28

Tutto quanto sopra funziona in modo terribile con lotti di grandi dimensioni o spazio di memoria insufficiente. Ho dovuto scrivere il mio che verrà pipeline (non notare alcun accumulo di articoli da nessuna parte):

public static class BatchLinq {
    public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size) {
        if (size <= 0)
            throw new ArgumentOutOfRangeException("size", "Must be greater than zero.");

        using (IEnumerator<T> enumerator = source.GetEnumerator())
            while (enumerator.MoveNext())
                yield return TakeIEnumerator(enumerator, size);
    }

    private static IEnumerable<T> TakeIEnumerator<T>(IEnumerator<T> source, int size) {
        int i = 0;
        do
            yield return source.Current;
        while (++i < size && source.MoveNext());
    }
}

Modifica: problema noto con questo approccio è che ogni batch deve essere enumerato ed enumerato completamente prima di passare al batch successivo. Ad esempio questo non funziona:

//Select first item of every 100 items
Batch(list, 100).Select(b => b.First())

1
La routine @LB pubblicata sopra non esegue nemmeno l'accumulo di articoli.
neontapir

2
@neontapir lo fa ancora. Una macchina per lo smistamento delle monete che ti dà prima i nickel, poi le monetine, DEVE prima ispezionare ogni singola moneta prima di darti un centesimo per essere sicuro che non ci siano più nickel.
Nick Whaley

2
Ahhh ahha, ho perso la tua nota di modifica quando ho rubato questo codice. Ci è voluto del tempo per capire perché l'iterazione su batch non enumerati ha effettivamente enumerato l'intera raccolta originale (!!!), fornendo X batch, ciascuno con 1 elemento (dove X è il numero di elementi della raccolta originale).
eli

2
@ NickWhaley se eseguo Count () sul risultato IEnumerable <IEnumerable <T>> dal codice, dà una risposta sbagliata, fornisce il numero totale di elementi, quando previsto è il numero totale di batch creati. Questo non è il caso del codice MoreLinq Batch
Mrinal Kamboj

1
@JohnZabroski - Ecco un breve riassunto
Matt Murrell

24

Si tratta di un'implementazione di Batch a una funzione completamente pigra, a basso overhead e che non fa alcun accumulo. Basato su (e risolto problemi nella) soluzione di Nick Whaley con l'aiuto di EricRoller.

L'iterazione proviene direttamente dall'IEnumerable sottostante, quindi gli elementi devono essere enumerati in ordine rigoroso e non è necessario accedervi più di una volta. Se alcuni elementi non vengono consumati in un ciclo interno, vengono scartati (e il tentativo di accedervi di nuovo tramite un iteratore salvato verrà generato InvalidOperationException: Enumeration already finished.).

Puoi testare un campione completo su .NET Fiddle .

public static class BatchLinq
{
    public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
    {
        if (size <= 0)
            throw new ArgumentOutOfRangeException("size", "Must be greater than zero.");
        using (var enumerator = source.GetEnumerator())
            while (enumerator.MoveNext())
            {
                int i = 0;
                // Batch is a local function closing over `i` and `enumerator` that
                // executes the inner batch enumeration
                IEnumerable<T> Batch()
                {
                    do yield return enumerator.Current;
                    while (++i < size && enumerator.MoveNext());
                }

                yield return Batch();
                while (++i < size && enumerator.MoveNext()); // discard skipped items
            }
    }
}

2
Questa è l'unica implementazione completamente pigra qui. Coerentemente con l'implementazione di python itertools.GroupBy.
Eric Roller

1
Puoi eliminare il controllo donesemplicemente chiamando sempre e.Count()dopo yield return e. Dovresti riorganizzare il ciclo in BatchInner per non richiamare il comportamento non definito source.Currentse i >= size. Ciò eliminerà la necessità di allocare un nuovo BatchInnerper ogni batch.
Eric Roller

1
Hai ragione, devi ancora acquisire informazioni sull'avanzamento di ogni batch. Ho trovato un bug nel tuo codice se provi a ottenere il secondo elemento da ogni batch: bug fiddle . L'implementazione fissa senza una classe separata (usando C # 7) è qui: fixed fiddle . Si noti che mi aspetto che CLR creerà ancora la funzione locale una volta per ciclo per acquisire la variabile, iquindi questo non è necessariamente più efficiente della definizione di una classe separata, ma credo sia un po 'più pulito.
Eric Roller

1
Ho confrontato questa versione utilizzando BenchmarkDotNet con System.Reactive.Linq.EnumerableEx.Buffer e la tua implementazione è stata 3-4 più veloce, a rischio della sicurezza. Internamente, EnumerableEx.Buffer alloca una coda di List <T> github.com/dotnet/reactive/blob/…
John Zabroski

1
Se vuoi una versione bufferizzata di questo, puoi fare: public static IEnumerable <IReadOnlyList <T>> BatchBuffered <T> (this IEnumerable <T> source, int size) => Batch (source, size) .Select (chunk = > (IReadOnlyList <T>) chunk.ToList ()); L'uso di IReadOnlyList <T> è per suggerire all'utente che l'output viene memorizzato nella cache. Potresti anche mantenere IEnumerable <IEnumerable <T>>.
gfache

12

Mi chiedo perché nessuno abbia mai pubblicato una soluzione for-loop della vecchia scuola. Eccone uno:

List<int> source = Enumerable.Range(1,23).ToList();
int batchsize = 10;
for (int i = 0; i < source.Count; i+= batchsize)
{
    var batch = source.Skip(i).Take(batchsize);
}

Questa semplicità è possibile perché il metodo Take:

... enumera sourcee restituisce elementi finché gli countelementi non sono stati restituiti o sourcenon contiene più elementi. Se countsupera il numero di elementi in source, sourcevengono restituiti tutti gli elementi di

Disclaimer:

L'uso di Skip and Take all'interno del ciclo significa che l'enumerabile verrà enumerato più volte. Questo è pericoloso se l'enumerabile viene differito. Potrebbe comportare più esecuzioni di una query di database, una richiesta Web o la lettura di un file. Questo esempio è esplicitamente per l'utilizzo di un elenco che non è differito, quindi è un problema minore. È ancora una soluzione lenta poiché skip enumererà la raccolta ogni volta che viene chiamata.

Questo può anche essere risolto utilizzando il GetRangemetodo, ma richiede un calcolo aggiuntivo per estrarre un possibile lotto di riposo:

for (int i = 0; i < source.Count; i += batchsize)
{
    int remaining = source.Count - i;
    var batch = remaining > batchsize  ? source.GetRange(i, batchsize) : source.GetRange(i, remaining);
}

Ecco un terzo modo per gestirlo, che funziona con 2 loop. Ciò garantisce che la raccolta venga enumerata solo 1 volta !:

int batchsize = 10;
List<int> batch = new List<int>(batchsize);

for (int i = 0; i < source.Count; i += batchsize)
{
    // calculated the remaining items to avoid an OutOfRangeException
    batchsize = source.Count - i > batchsize ? batchsize : source.Count - i;
    for (int j = i; j < i + batchsize; j++)
    {
        batch.Add(source[j]);
    }           
    batch.Clear();
}

2
Soluzione molto bella. La gente ha dimenticato come usare il ciclo for
VitalickS

1
L'uso di Skipe Takeall'interno del ciclo significa che l'enumerabile verrà enumerato più volte. Questo è pericoloso se l'enumerabile viene differito. Potrebbe comportare più esecuzioni di una query di database, una richiesta Web o la lettura di un file. Nel tuo esempio hai un Listche non è differito, quindi è un problema minore.
Theodor Zoulias

@ TheodorZoulias sì lo so, questo è in realtà il motivo per cui ho postato la seconda soluzione oggi. Ho postato il tuo commento come disclaimer, perché l'hai formulato abbastanza bene, posso citarti?
Mong Zhu,

Ho scritto una terza soluzione con 2 loop in modo che la raccolta venga enumerata solo 1 volta. la cosa skip.take è una soluzione molto inefficiente
Mong Zhu

4

Stesso approccio di MoreLINQ, ma utilizzando List invece di Array. Non ho eseguito il benchmarking, ma la leggibilità è più importante per alcune persone:

    public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
    {
        List<T> batch = new List<T>();

        foreach (var item in source)
        {
            batch.Add(item);

            if (batch.Count >= size)
            {
                yield return batch;
                batch.Clear();
            }
        }

        if (batch.Count > 0)
        {
            yield return batch;
        }
    }

1
NON dovresti riutilizzare la variabile batch. I tuoi consumatori potrebbero essere completamente incasinati da questo. Inoltre, passa il sizeparametro al tuo new Listper ottimizzarne le dimensioni.
ErikE,

1
batch.Clear();batch = new List<T>();
Soluzione

3

Ecco un tentativo di miglioramento delle implementazioni pigre di Nick Whaley ( link ) e di infogulch ( link ) Batch. Questo è rigoroso. Enumeri i batch nell'ordine corretto o ottieni un'eccezione.

public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
    this IEnumerable<TSource> source, int size)
{
    if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
    using (var enumerator = source.GetEnumerator())
    {
        int i = 0;
        while (enumerator.MoveNext())
        {
            if (i % size != 0) throw new InvalidOperationException(
                "The enumeration is out of order.");
            i++;
            yield return GetBatch();
        }
        IEnumerable<TSource> GetBatch()
        {
            while (true)
            {
                yield return enumerator.Current;
                if (i % size == 0 || !enumerator.MoveNext()) break;
                i++;
            }
        }
    }
}

Ed ecco Batchun'implementazione pigra per sorgenti di tipo IList<T>. Questo non impone restrizioni sull'enumerazione. I batch possono essere enumerati parzialmente, in qualsiasi ordine e più di una volta. Tuttavia, la restrizione di non modificare la raccolta durante l'enumerazione è ancora in vigore. Ciò si ottiene effettuando una chiamata fittizia a enumerator.MoveNext()prima di cedere qualsiasi blocco o elemento. Lo svantaggio è che l'enumeratore non viene smaltito, poiché non è noto quando l'enumerazione finirà.

public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
    this IList<TSource> source, int size)
{
    if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
    var enumerator = source.GetEnumerator();
    for (int i = 0; i < source.Count; i += size)
    {
        enumerator.MoveNext();
        yield return GetChunk(i, Math.Min(i + size, source.Count));
    }
    IEnumerable<TSource> GetChunk(int from, int toExclusive)
    {
        for (int j = from; j < toExclusive; j++)
        {
            enumerator.MoveNext();
            yield return source[j];
        }
    }
}

2

Mi unisco a questo molto tardi ma ho trovato qualcosa di più interessante.

Quindi possiamo usare qui Skipe Takeper prestazioni migliori.

public static class MyExtensions
    {
        public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> items, int maxItems)
        {
            return items.Select((item, index) => new { item, index })
                        .GroupBy(x => x.index / maxItems)
                        .Select(g => g.Select(x => x.item));
        }

        public static IEnumerable<T> Batch2<T>(this IEnumerable<T> items, int skip, int take)
        {
            return items.Skip(skip).Take(take);
        }

    }

Successivamente ho controllato con 100000 record. Solo il looping richiede più tempo in caso diBatch

Codice dell'applicazione console.

static void Main(string[] args)
{
    List<string> Ids = GetData("First");
    List<string> Ids2 = GetData("tsriF");

    Stopwatch FirstWatch = new Stopwatch();
    FirstWatch.Start();
    foreach (var batch in Ids2.Batch(5000))
    {
        // Console.WriteLine("Batch Ouput:= " + string.Join(",", batch));
    }
    FirstWatch.Stop();
    Console.WriteLine("Done Processing time taken:= "+ FirstWatch.Elapsed.ToString());


    Stopwatch Second = new Stopwatch();

    Second.Start();
    int Length = Ids2.Count;
    int StartIndex = 0;
    int BatchSize = 5000;
    while (Length > 0)
    {
        var SecBatch = Ids2.Batch2(StartIndex, BatchSize);
        // Console.WriteLine("Second Batch Ouput:= " + string.Join(",", SecBatch));
        Length = Length - BatchSize;
        StartIndex += BatchSize;
    }

    Second.Stop();
    Console.WriteLine("Done Processing time taken Second:= " + Second.Elapsed.ToString());
    Console.ReadKey();
}

static List<string> GetData(string name)
{
    List<string> Data = new List<string>();
    for (int i = 0; i < 100000; i++)
    {
        Data.Add(string.Format("{0} {1}", name, i.ToString()));
    }

    return Data;
}

Il tempo impiegato è così.

Primo - 00: 00: 00.0708, 00: 00: 00.0660

Secondo (Take and Skip One) - 00: 00: 00.0008, 00: 00: 00.0008


1
GroupByenumera completamente prima di produrre una singola riga. Questo non è un buon modo per eseguire il batch.
ErikE

@ErikE Dipende da cosa stai cercando di ottenere. Se il batching non è il problema e devi solo dividere gli elementi in blocchi più piccoli per l'elaborazione, potrebbe essere la cosa giusta. Lo sto usando per MSCRM dove potrebbero esserci 100 record, il che non è un problema per il batch LAMBDA ... è il salvataggio che richiede pochi secondi ..
JensB

1
Certo, ci sono casi d'uso in cui l'enumerazione completa non ha importanza. Ma perché scrivere un metodo di utilità di seconda classe quando puoi scriverne uno superbo?
ErikE,

Buona alternativa ma non identica in quanto prima restituisce un elenco di elenchi che consente di scorrere.
Gareth Hopkins,

passare foreach (var batch in Ids2.Batch(5000))a var gourpBatch = Ids2.Batch(5000)e controllare i risultati a tempo. o aggiungi elenco a var SecBatch = Ids2.Batch2(StartIndex, BatchSize);sarei interessato se i tuoi risultati per la temporizzazione cambiassero.
Seabizkit

2

Quindi, con un cappello funzionale, questo sembra banale ... ma in C # ci sono alcuni svantaggi significativi.

probabilmente lo vedresti come un unfold di IEnumerable (google e probabilmente finirai in alcuni documenti Haskell, ma potrebbero esserci alcune cose di F # usando unfold, se conosci F #, strizza gli occhi ai documenti Haskell e farà senso).

Unfold è correlato a fold ("aggregate") tranne che invece di iterare attraverso l'input IEnumerable, itera attraverso le strutture dei dati di output (è una relazione simile tra IEnumerable e IObservable, infatti penso che IObservable implementa un "unfold" chiamato generate. ..)

comunque prima hai bisogno di un metodo unfold, penso che funzioni (sfortunatamente alla fine farà saltare lo stack per grandi "liste" ... puoi scriverlo tranquillamente in F # usando yield! piuttosto che concat);

    static IEnumerable<T> Unfold<T, U>(Func<U, IEnumerable<Tuple<U, T>>> f, U seed)
    {
        var maybeNewSeedAndElement = f(seed);

        return maybeNewSeedAndElement.SelectMany(x => new[] { x.Item2 }.Concat(Unfold(f, x.Item1)));
    }

questo è un po 'ottuso perché C # non implementa alcune delle cose che i linguaggi funzionali danno per scontato ... ma fondamentalmente prende un seme e quindi genera una risposta "Forse" dell'elemento successivo in IEnumerable e il seme successivo (Forse non esiste in C #, quindi abbiamo usato IEnumerable per simularlo) e concatena il resto della risposta (non posso garantire la complessità "O (n?)" di questo).

Dopo averlo fatto, allora;

    static IEnumerable<IEnumerable<T>> Batch<T>(IEnumerable<T> xs, int n)
    {
        return Unfold(ys =>
            {
                var head = ys.Take(n);
                var tail = ys.Skip(n);
                return head.Take(1).Select(_ => Tuple.Create(tail, head));
            },
            xs);
    }

sembra tutto abbastanza pulito ... prendi gli elementi "n" come l'elemento "successivo" in IEnumerable, e la "coda" è il resto dell'elenco non elaborato.

se non c'è niente nella testa ... sei finito ... restituisci "Nothing" (ma simulato come un IEnumerable> vuoto) ... altrimenti restituisci l'elemento head e la coda da elaborare.

probabilmente puoi farlo usando IObservable, probabilmente c'è già un metodo simile a "Batch", e probabilmente puoi usarlo.

Se il rischio di overflow dello stack è preoccupante (probabilmente dovrebbe), allora dovresti implementarlo in F # (e probabilmente c'è già qualche libreria F # (FSharpX?) Con questo).

(Ho fatto solo alcuni test rudimentali su questo, quindi potrebbero esserci degli strani bug lì dentro).


1

Ho scritto un'implementazione IEnumerable personalizzata che funziona senza linq e garantisce una singola enumerazione sui dati. Inoltre, esegue tutto ciò senza richiedere elenchi o array di backup che causano esplosioni di memoria su set di dati di grandi dimensioni.

Ecco alcuni test di base:

    [Fact]
    public void ShouldPartition()
    {
        var ints = new List<int> {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
        var data = ints.PartitionByMaxGroupSize(3);
        data.Count().Should().Be(4);

        data.Skip(0).First().Count().Should().Be(3);
        data.Skip(0).First().ToList()[0].Should().Be(0);
        data.Skip(0).First().ToList()[1].Should().Be(1);
        data.Skip(0).First().ToList()[2].Should().Be(2);

        data.Skip(1).First().Count().Should().Be(3);
        data.Skip(1).First().ToList()[0].Should().Be(3);
        data.Skip(1).First().ToList()[1].Should().Be(4);
        data.Skip(1).First().ToList()[2].Should().Be(5);

        data.Skip(2).First().Count().Should().Be(3);
        data.Skip(2).First().ToList()[0].Should().Be(6);
        data.Skip(2).First().ToList()[1].Should().Be(7);
        data.Skip(2).First().ToList()[2].Should().Be(8);

        data.Skip(3).First().Count().Should().Be(1);
        data.Skip(3).First().ToList()[0].Should().Be(9);
    }

Il metodo di estensione per partizionare i dati.

/// <summary>
/// A set of extension methods for <see cref="IEnumerable{T}"/>. 
/// </summary>
public static class EnumerableExtender
{
    /// <summary>
    /// Splits an enumerable into chucks, by a maximum group size.
    /// </summary>
    /// <param name="source">The source to split</param>
    /// <param name="maxSize">The maximum number of items per group.</param>
    /// <typeparam name="T">The type of item to split</typeparam>
    /// <returns>A list of lists of the original items.</returns>
    public static IEnumerable<IEnumerable<T>> PartitionByMaxGroupSize<T>(this IEnumerable<T> source, int maxSize)
    {
        return new SplittingEnumerable<T>(source, maxSize);
    }
}

Questa è la classe di implementazione

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

    internal class SplittingEnumerable<T> : IEnumerable<IEnumerable<T>>
    {
        private readonly IEnumerable<T> backing;
        private readonly int maxSize;
        private bool hasCurrent;
        private T lastItem;

        public SplittingEnumerable(IEnumerable<T> backing, int maxSize)
        {
            this.backing = backing;
            this.maxSize = maxSize;
        }

        public IEnumerator<IEnumerable<T>> GetEnumerator()
        {
            return new Enumerator(this, this.backing.GetEnumerator());
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        private class Enumerator : IEnumerator<IEnumerable<T>>
        {
            private readonly SplittingEnumerable<T> parent;
            private readonly IEnumerator<T> backingEnumerator;
            private NextEnumerable current;

            public Enumerator(SplittingEnumerable<T> parent, IEnumerator<T> backingEnumerator)
            {
                this.parent = parent;
                this.backingEnumerator = backingEnumerator;
                this.parent.hasCurrent = this.backingEnumerator.MoveNext();
                if (this.parent.hasCurrent)
                {
                    this.parent.lastItem = this.backingEnumerator.Current;
                }
            }

            public bool MoveNext()
            {
                if (this.current == null)
                {
                    this.current = new NextEnumerable(this.parent, this.backingEnumerator);
                    return true;
                }
                else
                {
                    if (!this.current.IsComplete)
                    {
                        using (var enumerator = this.current.GetEnumerator())
                        {
                            while (enumerator.MoveNext())
                            {
                            }
                        }
                    }
                }

                if (!this.parent.hasCurrent)
                {
                    return false;
                }

                this.current = new NextEnumerable(this.parent, this.backingEnumerator);
                return true;
            }

            public void Reset()
            {
                throw new System.NotImplementedException();
            }

            public IEnumerable<T> Current
            {
                get { return this.current; }
            }

            object IEnumerator.Current
            {
                get { return this.Current; }
            }

            public void Dispose()
            {
            }
        }

        private class NextEnumerable : IEnumerable<T>
        {
            private readonly SplittingEnumerable<T> splitter;
            private readonly IEnumerator<T> backingEnumerator;
            private int currentSize;

            public NextEnumerable(SplittingEnumerable<T> splitter, IEnumerator<T> backingEnumerator)
            {
                this.splitter = splitter;
                this.backingEnumerator = backingEnumerator;
            }

            public bool IsComplete { get; private set; }

            public IEnumerator<T> GetEnumerator()
            {
                return new NextEnumerator(this.splitter, this, this.backingEnumerator);
            }

            IEnumerator IEnumerable.GetEnumerator()
            {
                return this.GetEnumerator();
            }

            private class NextEnumerator : IEnumerator<T>
            {
                private readonly SplittingEnumerable<T> splitter;
                private readonly NextEnumerable parent;
                private readonly IEnumerator<T> enumerator;
                private T currentItem;

                public NextEnumerator(SplittingEnumerable<T> splitter, NextEnumerable parent, IEnumerator<T> enumerator)
                {
                    this.splitter = splitter;
                    this.parent = parent;
                    this.enumerator = enumerator;
                }

                public bool MoveNext()
                {
                    this.parent.currentSize += 1;
                    this.currentItem = this.splitter.lastItem;
                    var hasCcurent = this.splitter.hasCurrent;

                    this.parent.IsComplete = this.parent.currentSize > this.splitter.maxSize;

                    if (this.parent.IsComplete)
                    {
                        return false;
                    }

                    if (hasCcurent)
                    {
                        var result = this.enumerator.MoveNext();

                        this.splitter.lastItem = this.enumerator.Current;
                        this.splitter.hasCurrent = result;
                    }

                    return hasCcurent;
                }

                public void Reset()
                {
                    throw new System.NotImplementedException();
                }

                public T Current
                {
                    get { return this.currentItem; }
                }

                object IEnumerator.Current
                {
                    get { return this.Current; }
                }

                public void Dispose()
                {
                }
            }
        }
    }

1

So che tutti usavano sistemi complessi per fare questo lavoro, e davvero non capisco perché. Take and skip consentirà tutte quelle operazioni utilizzando la Func<TSource,Int32,TResult>funzione di selezione comune con trasformazione. Piace:

public IEnumerable<IEnumerable<T>> Buffer<T>(IEnumerable<T> source, int size)=>
    source.Select((item, index) => source.Skip(size * index).Take(size)).TakeWhile(bucket => bucket.Any());

2
Questo potrebbe essere molto inefficiente, perché il dato sourceverrà ripetuto molto spesso.
Kevin Meier

1
Questo non solo è inefficiente, ma potrebbe anche produrre risultati errati. Non vi è alcuna garanzia che un enumerabile produrrà gli stessi elementi se enumerato due volte. Prendete questo enumerabile come esempio: Enumerable.Range(0, 1).SelectMany(_ => Enumerable.Range(0, new Random().Next())).
Theodor Zoulias

1

Solo un'altra implementazione di una riga. Funziona anche con un elenco vuoto, in questo caso si ottiene una raccolta batch di dimensioni zero.

var aList = Enumerable.Range(1, 100).ToList(); //a given list
var size = 9; //the wanted batch size
//number of batches are: (aList.Count() + size - 1) / size;

var batches = Enumerable.Range(0, (aList.Count() + size - 1) / size).Select(i => aList.GetRange( i * size, Math.Min(size, aList.Count() - i * size)));

Assert.True(batches.Count() == 12);
Assert.AreEqual(batches.ToList().ElementAt(0), new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
Assert.AreEqual(batches.ToList().ElementAt(1), new List<int>() { 10, 11, 12, 13, 14, 15, 16, 17, 18 });
Assert.AreEqual(batches.ToList().ElementAt(11), new List<int>() { 100 });

1

Un altro modo è usare l' operatore Rx Buffer

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

Non dovresti mai usare GetAwaiter().GetResult(). Questo è un odore di codice per il codice sincrono che chiama forzatamente il codice asincrono.
gfache

-2
    static IEnumerable<IEnumerable<T>> TakeBatch<T>(IEnumerable<T> ts,int batchSize)
    {
        return from @group in ts.Select((x, i) => new { x, i }).ToLookup(xi => xi.i / batchSize)
               select @group.Select(xi => xi.x);
    }

Aggiungi una descrizione / testo nella tua risposta. Mettere solo il codice può significare meno la maggior parte del tempo.
Ariful Haque
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.