Crie lotes no linq

104

Alguém pode sugerir uma maneira de criar lotes de um determinado tamanho no linq?

Idealmente, quero ser capaz de realizar operações em blocos de alguma quantidade configurável.

BlakeH
fonte

Respostas:

116

Você não precisa escrever nenhum código. Use o método MoreLINQ Batch, que agrupa a sequência de origem em intervalos de tamanho (MoreLINQ está disponível como um pacote NuGet que você pode instalar):

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

Que é implementado como:

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();
}
Sergey Berezovskiy
fonte
3
4 bytes por item tem um desempenho terrível ? Você tem alguns testes que mostram o que significa terrivelmente ? Se você estiver carregando milhões de itens na memória, eu não faria isso. Usar paginação do lado do servidor
Sergey Berezovskiy
4
Não é minha intenção ofendê-lo, mas existem soluções mais simples que não se acumulam. Além disso, isso alocará espaço até mesmo para elementos inexistentes:Batch(new int[] { 1, 2 }, 1000000)
Nick Whaley
7
@NickWhaley bem, concordo com você que espaço adicional será alocado, mas na vida real você geralmente tem a situação oposta - lista de 1000 itens que devem ir em lotes de 50 :)
Sergey Berezovskiy
1
Sim, a situação normalmente deveria ser o contrário, mas na vida real, podem ser entradas do usuário.
Nick Whaley
8
Esta é uma solução perfeitamente adequada. Na vida real, você: valida a entrada do usuário, trata os lotes como coleções inteiras de itens (o que acumula os itens de qualquer maneira) e, muitas vezes, processa os lotes em paralelo (o que não é suportado pela abordagem do iterador e será uma surpresa desagradável a menos que você saiba o detalhes de implementação).
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 o uso seria:

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

RESULTADO:

0,1,2
3,4,5
6,7,8
9
LIBRA
fonte
Funcionou perfeitamente para mim
FunMatters
16
Uma vez GroupByiniciada a enumeração, não é necessário enumerar totalmente sua fonte? Isso perde a avaliação preguiçosa da fonte e, portanto, em alguns casos, todos os benefícios do envio em lote!
ErikE
1
Nossa, obrigado, você me salvou da loucura. Funciona muito bem
Riaan de Lange
3
Como @ErikE menciona, este método enumera completamente sua fonte, embora pareça bom, ele vai contra o propósito de avaliação / pipelining preguiçoso
lasseschou
1
Faça isso - é totalmente apropriado quando você precisa dividir um bloco existente de coisas em lotes menores de coisas para processamento de desempenho. A alternativa é um loop de procura bruta, em que você divide os lotes manualmente e ainda passa por toda a fonte.
StingyJack
31

Se você começar com sequencedefinido como um IEnumerable<T>, e sabe que pode ser enumerado com segurança várias vezes (por exemplo, porque é uma matriz ou uma lista), você pode apenas usar este padrão simples para processar os elementos em lotes:

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

    // do whatever you need to do with each batch here
}
Matthew Strawbridge
fonte
2
Maneira
5
@DevHawk: é. Observe, entretanto, que o desempenho sofrerá exponencialmente em coleções grandes (r).
RobIII
28

Todos os itens acima têm um desempenho terrível com lotes grandes ou pouco espaço de memória. Tive que escrever o meu próprio que irá pipeline (não observe nenhum acúmulo de item em qualquer lugar):

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

Editar: O problema conhecido com essa abordagem é que cada lote deve ser enumerado e enumerado totalmente antes de passar para o próximo lote. Por exemplo, isso não funciona:

//Select first item of every 100 items
Batch(list, 100).Select(b => b.First())
Nick Whaley
fonte
1
A rotina @LB postada acima também não realiza acúmulo de itens.
neontapir de
2
@neontapir Ainda faz. Uma máquina de classificação de moedas que dá primeiro níqueis, depois dez centavos, DEVE primeiro inspecionar cada moeda antes de dar a você dez centavos para ter certeza de que não há mais moedas.
Nick Whaley
2
Ahhh ahha, perdi sua nota de edição quando peguei este código. Demorou algum tempo para entender por que a iteração em lotes não enumerados na verdade enumerava toda a coleção original (!!!), fornecendo X lotes, cada um tendo enumerado 1 item (onde X é o número de itens da coleção original).
eli
2
@NickWhaley se eu fizer Count () no resultado IEnumerable <IEnumerable <T>> pelo seu código, ele dá uma resposta errada, ele dá o número total de elementos, quando o esperado é o número total de lotes criados. Este não é o caso com o código MoreLinq Batch
Mrinal Kamboj
1
@JohnZabroski - Aqui está um resumo
Matt Murrell
24

Esta é uma implementação de uma função do Batch totalmente preguiçosa, com baixa sobrecarga e que não faz nenhum acúmulo. Baseado em (e corrige problemas na) solução de Nick Whaley com a ajuda de EricRoller.

A iteração vem diretamente do IEnumerable subjacente, portanto, os elementos devem ser enumerados em ordem estrita e acessados ​​no máximo uma vez. Se alguns elementos não forem consumidos em um loop interno, eles serão descartados (e tentar acessá-los novamente por meio de um iterador salvo será lançado InvalidOperationException: Enumeration already finished.).

Você pode testar uma amostra completa em .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
            }
    }
}
infogulch
fonte
2
Esta é a única implementação totalmente preguiçosa aqui. Consistente com a implementação python itertools.GroupBy.
Eric Roller
1
Você pode eliminar o cheque de donesempre pagando e.Count()depois yield return e. Você precisaria reorganizar o loop no BatchInner para não invocar o comportamento indefinido source.Currentse i >= size. Isso eliminará a necessidade de alocar um novo BatchInnerpara cada lote.
Eric Roller
1
Você tem razão, ainda precisa capturar informações sobre o andamento de cada lote. Eu encontrei um bug em seu código se você tentar pegar o segundo item de cada lote: bug fiddle . A implementação fixa sem uma classe separada (usando C # 7) está aqui: violino fixo . Observe que espero que o CLR ainda crie a função local uma vez por loop para capturar a variável, ientão isso não é necessariamente mais eficiente do que definir uma classe separada, mas acho que é um pouco mais limpo.
Eric Roller,
1
Eu comparei esta versão usando BenchmarkDotNet contra System.Reactive.Linq.EnumerableEx.Buffer e sua implementação foi 3-4 mais rápida, sob risco de segurança. Internamente, EnumerableEx.Buffer aloca uma Fila de Lista <T> github.com/dotnet/reactive/blob/…
John Zabroski
1
Se quiser uma versão em buffer disso, você pode fazer: public static IEnumerable <IReadOnlyList <T>> BatchBuffered <T> (this IEnumerable <T> source, int size) => Batch (source, size) .Select (chunk = > (IReadOnlyList <T>) chunk.ToList ()); O uso de IReadOnlyList <T> é para indicar ao usuário que a saída está armazenada em cache. Você também pode manter o IEnumerable <IEnumerable <T>> em vez disso.
gfache de
11

Eu me pergunto por que ninguém jamais postou uma solução para loop da velha escola. Aqui está um:

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

Essa simplicidade é possível porque o método Take:

... enumera sourcee produz elementos até que os countelementos tenham sido produzidos ou sourcenão contenha mais elementos. Se countexceder o número de elementos em source, todos os elementos de sourcesão retornados

Aviso Legal:

Usar Skip e Take dentro do loop significa que o enumerável será enumerado várias vezes. Isso é perigoso se o enumerável for adiado. Isso pode resultar em várias execuções de uma consulta de banco de dados, ou uma solicitação da web, ou uma leitura de arquivo. Este exemplo é explicitamente para o uso de uma Lista que não é adiada, portanto, é um problema menor. Ainda é uma solução lenta, pois skip enumerará a coleção sempre que for chamada.

Isso também pode ser resolvido usando o GetRangemétodo, mas requer um cálculo extra para extrair um possível lote restante:

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

Aqui está uma terceira maneira de lidar com isso, que funciona com 2 loops. Isso garante que a coleção seja enumerada apenas 1 vez !:

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();
}
Mong Zhu
fonte
2
Solução muito boa. As pessoas se esqueceram de como usar o loop for
VitalickS
1
Usar Skipe Takedentro do loop significa que o enumerável será enumerado várias vezes. Isso é perigoso se o enumerável for adiado. Isso pode resultar em várias execuções de uma consulta de banco de dados, ou uma solicitação da web, ou uma leitura de arquivo. No seu exemplo, você tem um Listque não é adiado, portanto, é menos problemático.
Theodor Zoulias
@TheodorZoulias sim, eu sei, na verdade é por isso que postei a segunda solução hoje. Publiquei seu comentário como isenção de responsabilidade, porque você o formulou muito bem, devo citá-lo?
Mong Zhu
Escrevi uma terceira solução com 2 loops para que a coleção fosse enumerada apenas 1 vez. a coisa do skip.take é uma solução muito ineficiente
Mong Zhu
4

Mesma abordagem que MoreLINQ, mas usando List em vez de Array. Não fiz benchmarking, mas a legibilidade é mais importante para algumas pessoas:

    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;
        }
    }
user4698855
fonte
1
Você NÃO deve reutilizar a variável de lote. Seus consumidores podem ficar completamente confusos com isso. Além disso, passe o sizeparâmetro new Listpara otimizar seu tamanho.
ErikE
1
Correção fácil: substituir batch.Clear();porbatch = new List<T>();
NetMage
3

Aqui está uma tentativa de melhoria das implementações preguiçosas de Nick Whaley ( link ) e infogulch ( link ) Batch. Este é estrito. Você enumera os lotes na ordem correta ou obtém uma exceção.

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

E aqui está uma Batchimplementação preguiçosa para fontes do tipo IList<T>. Este não impõe restrições à enumeração. Os lotes podem ser enumerados parcialmente, em qualquer ordem e mais de uma vez. A restrição de não modificar a coleção durante a enumeração ainda existe. Isso é conseguido fazendo uma chamada fictícia para enumerator.MoveNext()antes de produzir qualquer pedaço ou elemento. A desvantagem é que o enumerador não é eliminado, pois não se sabe quando a enumeração vai terminar.

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];
        }
    }
}
Theodor Zoulias
fonte
2

Estou entrando muito tarde, mas achei algo mais interessante.

Portanto, podemos usar aqui Skipe Takepara melhor desempenho.

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

    }

Em seguida, verifiquei com 100.000 registros. O loop só está levando mais tempo no caso deBatch

Código do aplicativo de 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;
}

O tempo gasto é assim.

Primeiro - 00: 00: 00.0708, 00: 00: 00.0660

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

Kaushik
fonte
1
GroupByenumera totalmente antes de produzir uma única linha. Essa não é uma boa maneira de fazer lotes.
ErikE
@ErikE Isso depende do que você está tentando alcançar. Se o lote não for o problema e você só precisar dividir os itens em blocos menores para processamento, talvez seja o ideal. Estou usando isso para MSCRM onde pode haver 100 registros, o que não é problema para o LAMBDA em lote .. é o salvamento que leva segundos ..
JensB
1
Claro, existem casos de uso em que a enumeração completa não importa. Mas por que escrever um método utilitário de segunda classe quando você pode escrever um excelente?
ErikE
Boa alternativa, mas não idêntica à primeira retorna uma lista de listas permitindo que você faça um loop.
Gareth Hopkins
mude foreach (var batch in Ids2.Batch(5000))para var gourpBatch = Ids2.Batch(5000)e verifique os resultados cronometrados. ou adicione tolist a var SecBatch = Ids2.Batch2(StartIndex, BatchSize);Eu estaria interessado se seus resultados para a mudança de tempo.
Seabizkit
2

Portanto, com um chapéu funcional, isso parece trivial ... mas em C #, existem algumas desvantagens significativas.

você provavelmente verá isso como um desdobramento de IEnumerable (pesquise no google e provavelmente acabará em alguns documentos do Haskell, mas pode haver algumas coisas do F # usando o desdobramento, se você conhece o F #, procure nos documentos do Haskell e isso fará sentido).

O desdobramento está relacionado a dobrar ("agregado"), exceto que, em vez de iterar por meio da entrada IEnumerable, ele itera por meio das estruturas de dados de saída (é uma relação semelhante entre IEnumerable e IObservable; na verdade, acho que IObservable implementa um "desdobrar" chamado gerar. ..)

de qualquer forma, primeiro você precisa de um método de desdobramento, eu acho que isso funciona (infelizmente ele acabará explodindo com grandes "listas" ... você pode escrever isso com segurança em F # usando yield! em vez de 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)));
    }

isto é um pouco obtuso porque C # não implementa algumas das coisas que as linguagens funcionais consideram certas ... mas basicamente pega uma semente e então gera uma resposta "Maybe" do próximo elemento no IEnumerable e a próxima semente (Maybe não existe em C #, então usamos IEnumerable para falsificá-lo) e concatena o resto da resposta (não posso garantir a complexidade "O (n?)" disso).

Depois de fazer isso;

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

tudo parece bastante limpo ... você pega os elementos "n" como o elemento "próximo" no IEnumerable, e a "cauda" é o resto da lista não processada.

se não há nada no head ... você acabou ... você retorna "Nothing" (mas fingido como um IEnumerable vazio>) ... senão você retorna o elemento head e o tail para processar.

você provavelmente pode fazer isso usando IObservable, provavelmente já existe um método do tipo "Lote" e você provavelmente pode usá-lo.

Se o risco de estouro de pilha preocupa (provavelmente deveria), então você deve implementar em F # (e provavelmente já existe alguma biblioteca F # (FSharpX?) Com isso).

(Eu só fiz alguns testes rudimentares disso, então pode haver alguns bugs estranhos lá).

MrD na KookerellaLtd
fonte
1

Eu escrevi uma implementação personalizada de IEnumerable que funciona sem o linq e garante uma única enumeração sobre os dados. Ele também realiza tudo isso sem exigir listas de apoio ou matrizes que causam explosões de memória em grandes conjuntos de dados.

Aqui estão alguns testes básicos:

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

O método de extensão para particionar os dados.

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

Esta é a aula de implementação

    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()
                {
                }
            }
        }
    }
leat
fonte
1

Eu sei que todo mundo usou sistemas complexos para fazer esse trabalho, e eu realmente não entendo por quê. Pegar e pular permitirá todas essas operações usando a seleção comum com a Func<TSource,Int32,TResult>função de transformação. Gostar:

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());
Johni Michels
fonte
2
Isso pode ser muito ineficiente, porque o dado sourceserá iterado com muita freqüência.
Kevin Meier
1
Isso não é apenas ineficiente, mas também pode produzir resultados incorretos. Não há garantia de que um enumerável produzirá os mesmos elementos quando enumerado duas vezes. Tome este enumeráveis como um exemplo: Enumerable.Range(0, 1).SelectMany(_ => Enumerable.Range(0, new Random().Next())).
Theodor Zoulias
1

Apenas mais uma implementação de uma linha. Funciona mesmo com uma lista vazia, neste caso você obtém uma coleção de lotes de tamanho 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 });
frhack
fonte
1

Outra maneira é usar o operador 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();
frhack
fonte
Você nunca deveria ter que usar GetAwaiter().GetResult(). Este é um cheiro de código para código síncrono chamando código assíncrono à força.
gfache de
-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);
    }
Nichom
fonte
Adicione alguma descrição / texto em sua resposta. Colocar apenas código pode significar menos na maioria das vezes.
Ariful Haque