Crear lotes en linq

¿Puede alguien sugerir una forma de crear lotes de cierto tamaño en linq?

Idealmente, quiero poder realizar operaciones en trozos de alguna cantidad configurable.

No necesita escribir ningún código. Use el método MoreLINQ Batch, que agrupa la secuencia fuente en cubos 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> Batch( this IEnumerable 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); } 
 public static class MyExtensions { public static IEnumerable> Batch(this IEnumerable 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 list = new List() { 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 

Todo lo anterior funciona terriblemente con lotes grandes o con poco espacio de memoria. Tuve que escribir el mío que se canalizará (nótese acumulación de elementos en ningún lugar):

 public static class BatchLinq { public static IEnumerable> Batch(this IEnumerable source, int size) { if (size <= 0) throw new ArgumentOutOfRangeException("size", "Must be greater than zero."); using (IEnumerator enumerator = source.GetEnumerator()) while (enumerator.MoveNext()) yield return TakeIEnumerator(enumerator, size); } private static IEnumerable TakeIEnumerator(IEnumerator 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()) 

Si comienza con la sequence definida como IEnumerable , y sabe que puede enumerarse varias veces de manera segura (por ejemplo, porque es una matriz o una lista), puede simplemente 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 } 

Se trata de una implementación de Batch completamente perezosa, de bajo costo y de una sola función que no acumula nada. 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 se deben enumerar en orden estricto y acceder a ellos no más de una vez. Si algunos elementos no se consumen en un bucle interno, se descartan (e intentar acceder a ellos nuevamente mediante un iterador guardado arrojará InvalidOperationException: Enumeration already finished. ).

Puede probar una muestra completa en .NET Fiddle .

 public static class BatchLinq { public static IEnumerable> Batch(this IEnumerable 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 Batch() { do yield return enumerator.Current; while (++i < size && enumerator.MoveNext()); } yield return Batch(); while (++i < size && enumerator.MoveNext()); // discard skipped items } } } 

Me estoy uniendo a esto muy tarde, pero encontré algo más interesante.

Entonces podemos usar Skip y Take aquí para un mejor rendimiento.

 public static class MyExtensions { public static IEnumerable> Batch(this IEnumerable 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 Batch2(this IEnumerable items, int skip, int take) { return items.Skip(skip).Take(take); } } 

Luego revisé con 100000 registros. El bucle solo toma más tiempo en caso de Batch

Código de aplicación de consola.

 static void Main(string[] args) { List Ids = GetData("First"); List 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 GetData(string name) { List Data = new List(); for (int i = 0; i < 100000; i++) { Data.Add(string.Format("{0} {1}", name, i.ToString())); } return Data; } 

El tiempo tomado es así.

Primero - 00: 00: 00.0708, 00: 00: 00.0660

Segundo (tomar y saltar uno) - 00: 00: 00,0008, 00: 00: 00,0008

El mismo enfoque que MoreLINQ, pero usando List en lugar de Array. No he hecho benchmarking, pero la legibilidad es más importante para algunas personas:

  public static IEnumerable> Batch(this IEnumerable source, int size) { List batch = new List(); foreach (var item in source) { batch.Add(item); if (batch.Count >= size) { yield return batch; batch.Clear(); } } if (batch.Count > 0) { yield return batch; } } 

Así que con un sombrero funcional encendido, esto parece trivial … pero en C #, hay algunas desventajas significativas.

probablemente verías esto como un despliegue de IEnumerable (googleándolo y probablemente terminarás en algunos documentos de Haskell, pero puede haber algo de F # usando desplegar, si sabes F #, entrecerrar los documentos de Haskell y hará sentido).

Despliegue está relacionado con fold (“agregado”) 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 generar. ..)

de todos modos, primero necesitas un método desplegable, creo que esto funciona;

  static IEnumerable Unfold(Func>> 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 el IEnumerable y la siguiente semilla (Maybe no existe en C #, por lo que hemos utilizado IEnumerable para simularlo) y concatena el rest de la respuesta (no puedo responder por la complejidad “O (n?)” de esto).

Una vez que hayas hecho eso, entonces;

  static IEnumerable> Batch(IEnumerable 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 … toma los elementos “n” como el elemento “siguiente” en IEnumerable, y la “cola” es el rest de la lista no procesada.

si no hay nada en la cabeza … has terminado … devuelves “Nada” (pero simulado como un IEnumerable vacío) … si no, devuelves el elemento principal y la cola para procesar.

probablemente puedas hacer esto usando IObservable, probablemente ya haya un método similar al “Batch”, y probablemente puedas usar eso.

Si el riesgo de la stack desborda preocupaciones (probablemente debería), entonces debe implementar en F # (y probablemente haya alguna biblioteca F # (¿FSharpX?) Ya con esto).

(Solo he hecho algunas pruebas rudimentarias de esto, por lo que puede haber errores extraños).

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 {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.

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

Esta es la clase de implementación

  using System.Collections; using System.Collections.Generic; internal class SplittingEnumerable : IEnumerable> { private readonly IEnumerable backing; private readonly int maxSize; private bool hasCurrent; private T lastItem; public SplittingEnumerable(IEnumerable backing, int maxSize) { this.backing = backing; this.maxSize = maxSize; } public IEnumerator> GetEnumerator() { return new Enumerator(this, this.backing.GetEnumerator()); } IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } private class Enumerator : IEnumerator> { private readonly SplittingEnumerable parent; private readonly IEnumerator backingEnumerator; private NextEnumerable current; public Enumerator(SplittingEnumerable parent, IEnumerator 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 Current { get { return this.current; } } object IEnumerator.Current { get { return this.Current; } } public void Dispose() { } } private class NextEnumerable : IEnumerable { private readonly SplittingEnumerable splitter; private readonly IEnumerator backingEnumerator; private int currentSize; public NextEnumerable(SplittingEnumerable splitter, IEnumerator backingEnumerator) { this.splitter = splitter; this.backingEnumerator = backingEnumerator; } public bool IsComplete { get; private set; } public IEnumerator GetEnumerator() { return new NextEnumerator(this.splitter, this, this.backingEnumerator); } IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } private class NextEnumerator : IEnumerator { private readonly SplittingEnumerable splitter; private readonly NextEnumerable parent; private readonly IEnumerator enumerator; private T currentItem; public NextEnumerator(SplittingEnumerable splitter, NextEnumerable parent, IEnumerator 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() { } } } } 
  static IEnumerable> TakeBatch(IEnumerable 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); }