Crear lotes en linq

104

¿Alguien puede sugerir una forma de crear lotes de cierto tamaño en linq?

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

BlakeH
fuente

Respuestas:

116

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();
}
Sergey Berezovskiy
fuente
3
¿4 bytes por elemento funcionan terriblemente ? ¿Tiene algunas pruebas que demuestren lo que significa terriblemente ? Si está cargando millones de elementos en la memoria, no lo haría. Usar paginación del lado del servidor
Sergey Berezovskiy
4
No quiero ofenderlos, pero hay soluciones más simples que no se acumulan en absoluto. Además, esto asignará espacio incluso para elementos inexistentes:Batch(new int[] { 1, 2 }, 1000000)
Nick Whaley
7
@NickWhaley bueno, estoy de acuerdo con usted en que se asignará espacio adicional, pero en la vida real generalmente tiene una situación opuesta: una lista de 1000 elementos que deberían ir en lotes de 50 :)
Sergey Berezovskiy
1
Sí, la situación debería ser al revés, pero en la vida real, estas pueden ser entradas del usuario.
Nick Whaley
8
Esta es una solución perfectamente buena. En la vida real, usted: valida la entrada del usuario, trata los lotes como colecciones completas de elementos (que acumula los elementos de todos modos) y, a menudo, procesa los lotes en paralelo (lo que no es compatible con el enfoque del iterador, y será una sorpresa desagradable a menos que sepa el detalles de implementacion).
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));
    }
}

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
LB
fuente
Perfecta trabajado para mí
FunMatters
16
Una vez que GroupBycomienza 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!
ErikE
1
Vaya, gracias, me salvaste de la locura. Funciona muy bien
Riaan de Lange
3
Como menciona @ErikE, este método enumera completamente su fuente, por lo que, aunque se ve bien,
frustra
1
Haga esto: es totalmente apropiado cuando necesita dividir un bloque existente de cosas en lotes más pequeños de cosas para un procesamiento eficaz. La alternativa es un bucle de búsqueda bruto en el que se dividen manualmente los lotes y aún se recorre toda la fuente.
StingyJack
31

Si comienza con sequencedefinido 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
}
Matthew Strawbridge
fuente
2
Manera agradable y sencilla de procesamiento por lotes sin mucho código o necesidad de una biblioteca externa
DevHawk
5
@DevHawk: lo es. Sin embargo, tenga en cuenta que el rendimiento se verá afectado exponencialmente en colecciones grandes (r).
RobIII
28

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())
Nick Whaley
fuente
1
La rutina @LB publicada anteriormente tampoco realiza la acumulación de artículos.
neontapir
2
@neontapir Todavía lo hace. Una máquina clasificadora de monedas que le da cinco centavos primero, luego monedas de diez centavos, DEBE inspeccionar primero cada moneda antes de darle un centavo para asegurarse de que no haya más monedas de cinco centavos.
Nick Whaley
2
Ahhh ahha, perdí tu nota de edición cuando enganché este código. Tomó algún tiempo entender por qué iterar sobre lotes no enumerados en realidad enumeraba toda la colección original (!!!), proporcionando X lotes, cada uno con 1 elemento enumerado (donde X es el número de elementos de la colección original).
eli
2
@NickWhaley si hago Count () en el resultado IEnumerable <IEnumerable <T>> de su código, da una respuesta incorrecta, da el número total de elementos, cuando se espera es el número total de lotes creados. Este no es el caso con el código de MoreLinq Batch
Mrinal Kamboj
1
@JohnZabroski - Aquí hay una breve descripción
Matt Murrell
24

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
            }
    }
}
infogulch
fuente
2
Esta es la única implementación completamente perezosa aquí. De acuerdo con la implementación de itertools.GroupBy de Python.
Eric Roller
1
Puede eliminar el cheque donesimplemente llamando siempre e.Count()después yield return e. Debería reorganizar el bucle en BatchInner para no invocar el comportamiento indefinido source.Currentsi i >= size. Esto eliminará la necesidad de asignar uno nuevo BatchInnerpara cada lote.
Eric Roller
1
Tiene razón, todavía necesita capturar información sobre el progreso de cada lote. Encontré un error en su código si intenta obtener el segundo elemento de cada lote: bug fiddle . La implementación fija sin una clase separada (usando C # 7) está aquí: violín fijo . Tenga en cuenta que espero que CLR aún cree la función local una vez por ciclo para capturar la variable, ipor lo que esto no es necesariamente más eficiente que definir una clase separada, pero creo que es un poco más limpio.
Eric Roller
1
Evalué esta versión usando BenchmarkDotNet con System.Reactive.Linq.EnumerableEx.Buffer y su implementación fue 3-4 más rápida, a riesgo de seguridad. Internamente, EnumerableEx.Buffer asigna una cola de lista <T> github.com/dotnet/reactive/blob/…
John Zabroski
1
Si desea una versión en búfer de esto, puede hacer: public static IEnumerable <IReadOnlyList <T>> BatchBuffered <T> (esta fuente IEnumerable <T>, int size) => Batch (source, size) .Select (chunk = > (IReadOnlyList <T>) chunk.ToList ()); El uso de IReadOnlyList <T> es para indicarle al usuario que la salida está almacenada en caché. En su lugar, también podría mantener IEnumerable <IEnumerable <T>>.
gfache
11

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 sourcey produce elementos hasta countque se han producido elementos o sourceno contiene más elementos. Si countsupera el número de elementos de source, sourcese 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 GetRangemé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();
}
Mong Zhu
fuente
2
Muy buena solucion. La gente olvidó cómo usar el bucle for
VitalickS
1
Usar Skipy Takedentro 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 Listque no se aplaza, por lo que es un problema menor.
Theodor Zoulias
@TheodorZoulias sí, lo sé, en realidad es por eso que publiqué la segunda solución hoy. Publiqué su comentario como descargo de responsabilidad, porque lo formuló bastante bien, ¿puedo citarlo?
Mong Zhu
Escribí una tercera solución con 2 bucles para que la colección se enumere solo 1 vez. la cosa skip.take es una solución muy ineficiente
Mong Zhu
4

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;
        }
    }
usuario4698855
fuente
1
NO debería reutilizar la variable por lotes. Tus consumidores podrían estar completamente arruinados por eso. Además, pase el sizeparámetro a su new Listpara optimizar su tamaño.
ErikE
1
batch.Clear();batch = new List<T>();
Solución
3

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 Batchimplementació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];
        }
    }
}
Theodor Zoulias
fuente
2

Me uniré a esto muy tarde pero encontré algo más interesante.

Entonces podemos usar aquí Skipy Takepara 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

Kaushik
fuente
1
GroupByenumera completamente antes de producir una sola fila. Esta no es una buena forma de realizar lotes.
ErikE
@ErikE Eso depende de lo que estés tratando de lograr. Si el lote no es el problema, y ​​solo necesita dividir los artículos en trozos más pequeños para procesarlos, podría ser la solución. Estoy usando esto para MSCRM donde podría haber 100 registros, lo cual no es un problema para que LAMBDA procese por lotes ... es el ahorro que toma segundos ...
JensB
1
Claro, hay casos de uso en los que la enumeración completa no importa. Pero, ¿por qué escribir un método de utilidad de segunda clase cuando puedes escribir uno excelente?
ErikE
Buena alternativa pero no idéntica a la primera que devuelve una lista de listas que le permite recorrerlas.
Gareth Hopkins
cambiar 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.
Seabizkit
2

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í).

MrD en KookerellaLtd
fuente
1

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

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());
Johni Michels
fuente
2
Esto puede resultar muy ineficaz, porque lo dado se sourcerepetirá con mucha frecuencia.
Kevin Meier
1
Esto no solo es ineficiente, sino que también podría producir resultados incorrectos. No hay garantía de que un enumerable produzca los mismos elementos cuando se enumera dos veces. Tome este enumerable como ejemplo: Enumerable.Range(0, 1).SelectMany(_ => Enumerable.Range(0, new Random().Next())).
Theodor Zoulias
1

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 });
frhack
fuente
1

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();
frhack
fuente
Nunca debería tener que usar GetAwaiter().GetResult(). Este es un olor a código para código síncrono que llama forzosamente a código asíncrono.
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);
    }
nichom
fuente
Agregue alguna descripción / texto en su respuesta. Poner solo código puede significar menos la mayor parte del tiempo.
Ariful Haque