Respuestas:
No es necesario que escriba ningún código. Use el método MoreLINQ Batch, que agrupa la secuencia de origen en depósitos de tamaño (MoreLINQ está disponible como un paquete NuGet que puede instalar):
int size = 10;
var batches = sequence.Batch(size);
Que se implementa 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));
}
}
y el uso sería:
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));
}
SALIDA:
0,1,2
3,4,5
6,7,8
9
GroupBy
comienza la enumeración, ¿no tiene que enumerar completamente su fuente? Esto pierde la evaluación perezosa de la fuente y, por lo tanto, en algunos casos, ¡todos los beneficios del procesamiento por lotes!
Si comienza con sequence
definido como an IEnumerable<T>
, y sabe que se puede enumerar de manera segura varias veces (por ejemplo, porque es una matriz o una lista), puede usar este patrón simple para procesar los elementos en lotes:
while (sequence.Any())
{
var batch = sequence.Take(10);
sequence = sequence.Skip(10);
// do whatever you need to do with each batch here
}
Todo lo anterior funciona terriblemente con lotes grandes o con poco espacio de memoria. Tuve que escribir el mío propio que se canalizará (observe que no hay acumulación de artículos en ninguna 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());
}
}
Editar: el problema conocido con este enfoque es que cada lote debe enumerarse y enumerarse por completo antes de pasar al siguiente lote. Por ejemplo, esto no funciona:
//Select first item of every 100 items
Batch(list, 100).Select(b => b.First())
Esta es una implementación de Batch completamente perezosa, de baja sobrecarga y de una función que no hace ninguna acumulación. Basado en (y soluciona problemas en) la solución de Nick Whaley con la ayuda de EricRoller.
La iteración proviene directamente del IEnumerable subyacente, por lo que los elementos deben enumerarse en orden estricto y no se debe acceder a ellos más de una vez. Si algunos elementos no se consumen en un bucle interno, se descartan (e intentar acceder a ellos nuevamente a través de un iterador guardado arrojará InvalidOperationException: Enumeration already finished.
).
Puede probar una muestra completa en .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
simplemente llamando siempre e.Count()
después yield return e
. Debería reorganizar el bucle en BatchInner para no invocar el comportamiento indefinido source.Current
si i >= size
. Esto eliminará la necesidad de asignar uno nuevo BatchInner
para cada lote.
i
por lo que esto no es necesariamente más eficiente que definir una clase separada, pero creo que es un poco más limpio.
Me pregunto por qué nadie ha publicado nunca una solución de ciclo forzado de la vieja escuela. Acá hay 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);
}
Esta simplicidad es posible porque el método Take:
... enumera
source
y produce elementos hastacount
que se han producido elementos osource
no contiene más elementos. Sicount
supera el número de elementos desource
,source
se devuelven todos los elementos de
Descargo de responsabilidad:
El uso de Skip y Take dentro del bucle significa que el enumerable se enumerará varias veces. Esto es peligroso si se aplaza el enumerable. Puede resultar en varias ejecuciones de una consulta de base de datos, una solicitud web o la lectura de un archivo. Este ejemplo es explícitamente para el uso de una Lista que no se aplaza, por lo que es un problema menor. Sigue siendo una solución lenta, ya que skip enumerará la colección cada vez que se llame.
Esto también se puede resolver usando el GetRange
método, pero requiere un cálculo adicional para extraer un posible lote de reposo:
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);
}
Aquí hay una tercera forma de manejar esto, que funciona con 2 bucles. ¡Esto asegura que la colección se enumere solo 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
y Take
dentro del bucle significa que el enumerable se enumerará varias veces. Esto es peligroso si se aplaza el enumerable. Puede resultar en varias ejecuciones de una consulta de base de datos, una solicitud web o la lectura de un archivo. En su ejemplo, tiene una List
que no se aplaza, por lo que es un problema menor.
El mismo enfoque que MoreLINQ, pero usando List en lugar de Array. No he realizado evaluaciones comparativas, pero la legibilidad es más importante para algunas personas:
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 a su new List
para optimizar su tamaño.
batch.Clear();
batch = new List<T>();
Aquí hay un intento de mejora de las implementaciones perezosas de Nick Whaley ( enlace ) e infogulch ( enlace ) Batch
. Éste es estricto. O enumera los lotes en el orden correcto o obtiene una excepción.
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++;
}
}
}
}
Y aquí hay una Batch
implementación perezosa para fuentes de tipo IList<T>
. Éste no impone restricciones a la enumeración. Los lotes se pueden enumerar parcialmente, en cualquier orden y más de una vez. Sin embargo, la restricción de no modificar la colección durante la enumeración sigue vigente. Esto se logra haciendo una llamada ficticia a enumerator.MoveNext()
antes de ceder cualquier fragmento o elemento. La desventaja es que el enumerador se deja sin disposición, ya que se desconoce cuándo terminará la enumeración.
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];
}
}
}
Me uniré a esto muy tarde pero encontré algo más interesante.
Entonces podemos usar aquí Skip
y Take
para un mejor rendimiento.
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);
}
}
Luego verifiqué con 100000 registros. El bucle solo está tomando más tiempo en caso deBatch
Código de la aplicación de consola.
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;
}
El tiempo necesario es así.
Primero - 00: 00: 00.0708, 00: 00: 00.0660
Segundo (tomar y omitir uno) - 00: 00: 00.0008, 00: 00: 00.0008
GroupBy
enumera completamente antes de producir una sola fila. Esta no es una buena forma de realizar lotes.
foreach (var batch in Ids2.Batch(5000))
a var gourpBatch = Ids2.Batch(5000)
y comprobar los resultados cronometrados. o agregue tolist a Me var SecBatch = Ids2.Batch2(StartIndex, BatchSize);
interesaría si sus resultados de sincronización cambian.
Entonces, con un sombrero funcional, esto parece trivial ... pero en C #, hay algunas desventajas importantes.
probablemente verá esto como un despliegue de IEnumerable (busque en Google y probablemente terminará en algunos documentos de Haskell, pero puede haber algunas cosas de F # usando desplegar, si conoce F #, entrecerre los ojos en los documentos de Haskell y hará sentido).
Unfold está relacionado con fold ("aggregate") excepto que en lugar de iterar a través de la entrada IEnumerable, itera a través de las estructuras de datos de salida (es una relación similar entre IEnumerable e IObservable, de hecho creo que IObservable implementa un "despliegue" llamado generate. ..)
de todos modos, primero necesita un método de despliegue, creo que esto funciona (desafortunadamente, eventualmente volará la pila para "listas" grandes ... puede escribir esto de manera segura en F # usando yield! en lugar 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)));
}
esto es un poco obtuso porque C # no implementa algunas de las cosas que los lenguajes funcionales dan por sentado ... pero básicamente toma una semilla y luego genera una respuesta "Quizás" del siguiente elemento en IEnumerable y la siguiente semilla (Quizás no existe en C #, por lo que hemos usado IEnumerable para falsificarlo), y concatena el resto de la respuesta (no puedo responder por la complejidad "O (n?)" de esto).
Una vez que haya hecho eso, entonces;
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);
}
todo parece bastante limpio ... se toman los elementos "n" como el elemento "siguiente" en IEnumerable, y la "cola" es el resto de la lista sin procesar.
si no hay nada en la cabeza ... se acabó ... devuelve "Nada" (pero falsificado como un IEnumerable> vacío) ... de lo contrario, devuelve el elemento de la cabeza y la cola para procesar.
probablemente pueda hacer esto usando IObservable, probablemente ya haya un método similar a "Batch", y probablemente pueda usarlo.
Si el riesgo de desbordamiento de la pila le preocupa (probablemente debería), entonces debería implementar en F # (y probablemente ya haya alguna biblioteca F # (FSharpX?) Con esto).
(Solo he hecho algunas pruebas rudimentarias de esto, por lo que puede haber errores extraños allí).
Escribí una implementación personalizada de IEnumerable que funciona sin linq y garantiza una sola enumeración sobre los datos. También logra todo esto sin requerir listas de respaldo o matrices que causan explosiones de memoria en grandes conjuntos de datos.
Aquí hay algunas pruebas básicas:
[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);
}
El método de extensión para particionar los datos.
/// <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 es la clase de implementación
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()
{
}
}
}
}
Sé que todos usaron sistemas complejos para hacer este trabajo, y realmente no entiendo por qué. Tomar y omitir permitirá todas esas operaciones utilizando la Func<TSource,Int32,TResult>
función de selección común con transformación. Me gusta:
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
repetirá con mucha frecuencia.
Enumerable.Range(0, 1).SelectMany(_ => Enumerable.Range(0, new Random().Next()))
.
Solo otra implementación de una línea. Funciona incluso con una lista vacía, en este caso obtiene una colección de lotes de tamaño cero.
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 });
Otra forma es usar el 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 es un olor a código para código síncrono que llama forzosamente a código asíncrono.
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);
}