Respostas:
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();
}
Batch(new int[] { 1, 2 }, 1000000)
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
GroupBy
iniciada 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!
Se você começar com sequence
definido 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
}
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())
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
}
}
}
done
sempre pagando e.Count()
depois yield return e
. Você precisaria reorganizar o loop no BatchInner para não invocar o comportamento indefinido source.Current
se i >= size
. Isso eliminará a necessidade de alocar um novo BatchInner
para cada lote.
i
então isso não é necessariamente mais eficiente do que definir uma classe separada, mas acho que é um pouco mais limpo.
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
source
e produz elementos até que oscount
elementos tenham sido produzidos ousource
não contenha mais elementos. Secount
exceder o número de elementos emsource
, todos os elementos desource
sã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 GetRange
mé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();
}
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. No seu exemplo, você tem um List
que não é adiado, portanto, é menos problemático.
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;
}
}
size
parâmetro new List
para otimizar seu tamanho.
batch.Clear();
porbatch = new List<T>();
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 Batch
implementaçã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];
}
}
}
Estou entrando muito tarde, mas achei algo mais interessante.
Portanto, podemos usar aqui Skip
e Take
para 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
GroupBy
enumera totalmente antes de produzir uma única linha. Essa não é uma boa maneira de fazer lotes.
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.
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á).
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()
{
}
}
}
}
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());
source
será iterado com muita freqüência.
Enumerable.Range(0, 1).SelectMany(_ => Enumerable.Range(0, new Random().Next()))
.
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 });
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();
GetAwaiter().GetResult()
. Este é um cheiro de código para código síncrono chamando código assíncrono à força.
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);
}