Створюйте партії в linq


104

Чи може хтось запропонувати спосіб створення партій певного розміру в linq?

В ідеалі я хочу, щоб я мав змогу виконувати операції в кусках певної кількості, що можна настроювати.

Відповіді:


116

Вам не потрібно писати будь-який код. Використовуйте пакетний метод MoreLINQ , який збирає послідовність джерел у розміри відра (MoreLINQ доступний як пакет NuGet, який ви можете встановити):

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

Що реалізується як:

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

3
4 байти на предмет виконує жахливо ? Чи є у вас якісь тести, які показують, що страшно означає? Якщо ви завантажуєте в пам'ять мільйони предметів, то я б цього не робив. Використовуйте підказка на стороні сервера
Сергій Березовський,

4
Я не хочу вас ображати, але є більш прості рішення, які взагалі не накопичуються. Крім того, це виділить простір навіть для неіснуючих елементів:Batch(new int[] { 1, 2 }, 1000000)
Нік Уейлі

7
@NickWhaley добре, погоджуйся з тобою, що буде виділено додатковий простір, але в реальному житті у тебе зазвичай протилежна ситуація - перелік 1000 предметів, які повинні скластись по 50 партій :)
Сергій Березовський

1
Так, зазвичай ситуація має бути іншою стороною, але в реальному житті це можуть бути дані користувачів.
Nick Whaley

8
Це ідеально прекрасне рішення. У реальному житті ви: перевіряєте введення користувача, ставитеся до пакетів як до цілих колекцій елементів (які все одно накопичують елементи) і часто обробляєте партії паралельно (що не підтримується ітераційним підходом, і буде неприємним сюрпризом, якщо ви не знаєте деталі реалізації).
Майкл Петіто

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

і використання було б:

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

ВИХІД:

0,1,2
3,4,5
6,7,8
9

Працював ідеально для мене
FunMatters

16
Після того, як GroupByпочинається перерахування, чи не потрібно повністю перераховувати його джерело? Це втрачає ледачу оцінку джерела, а отже, в деяких випадках і всю користь від видобутку!
ErikE

1
Нічого, дякую, ти врятував мене від божевілля. Працює дуже добре
Riaan de Lange

3
Як згадує @ErikE, цей метод повністю перераховує своє джерело, тож хоч і виглядає добре, але перемагає мету ледачого оцінювання / конвеєризації
lasseschou

1
Зробіть це - це цілком доречно, коли вам потрібно розбити існуючий блок речей на більш дрібні партії речей для виконуючої обробки. Альтернатива - це грубі пошуки циклу, де ви вручну розбиваєте партії.
StingyJack

31

Якщо ви почнете з sequenceвизначеного як an IEnumerable<T>, і ви знаєте, що його можна сміливо перераховувати кілька разів (наприклад, тому, що це масив або список), ви можете просто використовувати цю просту схему для обробки елементів в партіях:

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

    // do whatever you need to do with each batch here
}

2
Хороший, простий спосіб отримання без коду чи необхідності зовнішньої бібліотеки
DevHawk

5
@DevHawk: так і є. Однак зауважте, що продуктивність буде страждати експоненціально від великих (r) колекцій.
RobIII

28

Все вищесказане працює надзвичайно великими партіями або мало місця в пам'яті. Довелося написати моє власне, що буде прокладено (не помічайте ніде накопичення елементів):

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

Редагувати: Відома проблема цього підходу полягає в тому, що кожна партія повинна бути перерахована і перерахована повністю перед переходом до наступної партії. Наприклад, це не працює:

//Select first item of every 100 items
Batch(list, 100).Select(b => b.First())

1
Опублікований вище звичайний @LB також не здійснює накопичення елементів.
neontapir

2
@neontapir Все ще. Машина для сортування монет, яка спочатку дає вам нікелі, потім димеси, ОБОВ'ЯЗКОВО слід спочатку оглянути кожну монету, перш ніж дати вам ні копійки, щоб бути впевненим, що більше нікелів немає.
Nick Whaley

2
А-а-а-а, пропустив вашу примітку до редагування, коли я зашивав цей код. Минув певний час, щоб зрозуміти, чому ітерація над не переліченими партіями насправді перераховувала всю оригінальну колекцію (!!!), надаючи X партій, кожна з яких перерахувала 1 елемент (де X - кількість оригінальних предметів колекції).
елі

2
@ NickWhaley, якщо я підрахую результат (IE) до IEnumerable <IEnumerable <T>> за вашим кодом, він дає неправильну відповідь, він дає загальну кількість елементів, коли очікується загальна кількість створених партій. Це не так з кодом партії MoreLinq
Mrinal Kamboj

1
@JohnZabroski - Ось швидкий зміст
Метт Меррелл

24

Це повністю ледача, низька накладні витрати, однофункціональна реалізація Batch, яка не накопичує. На основі (і виправляє проблеми в) Нік Уейл в розчині за допомогою EricRoller.

Ітерація надходить безпосередньо від основної IEnumerable, тому елементи повинні бути перераховані в суворому порядку і доступ до них не більше одного разу. Якщо деякі елементи не споживаються у внутрішньому циклі, вони відкидаються (і спроба знову отримати доступ до них через збережений ітератор буде кинутий InvalidOperationException: Enumeration already finished.).

Ви можете протестувати повний зразок у .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
            }
    }
}

2
Це єдина тут повністю лінива реалізація. Відповідно до реалізації itttools python.GroupBy.
Ерік Ролер

1
Ви можете усунути чек done, просто зателефонувавши e.Count()після yield return e. Вам потрібно буде переставити цикл у BatchInner, щоб не викликати невизначене поведінку, source.Currentякщо i >= size. Це позбавить від необхідності виділяти нову BatchInnerдля кожної партії.
Ерік Ролер

1
Ви маєте рацію, вам все одно потрібно збирати інформацію про хід кожної партії. Я знайшов помилку у вашому коді, якщо ви спробуєте отримати 2-й елемент з кожної партії: скрипка помилки . Фіксована реалізація без окремого класу (з використанням C # 7) знаходиться тут: фіксована скрипка . Зауважте, що я очікую, що CLR все одно створить локальну функцію один раз на цикл для зйомки змінної, iтому це не обов'язково більш ефективно, ніж визначення окремого класу, але я думаю, що це трохи чистіше.
Ерік Роллер

1
Я порівняв цю версію за допомогою BenchmarkDotNet проти System.Reactive.Linq.EnumerableEx.Buffer і ваша реалізація була на 3-4 швидше, ризикуючи безпекою. Внутрішньо, EnumerableEx.Buffer виділяє Чергу списку <T> github.com/dotnet/reactive/blob/…
Джон Заброський

1
Якщо вам потрібна буферизована версія цього, ви можете зробити: public static IEnumerable <IReadOnlyList <T>> BatchBuffered <T> (це джерело IEnumerable <T>, int size) => Batch (джерело, розмір) .Select (chunk = > (IReadOnlyList <T>) фрагмент.ToList ()); Використання IReadOnlyList <T> - це натякнути користувачеві на вихід кешування. Ви також можете зберегти IEnumerable <IEnumerable <T>> замість цього.
gfache

11

Цікаво, чому ніхто ніколи не розміщував стару школу для вирішення проблем. Ось один:

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

Ця простота можлива, оскільки метод Take:

... перераховує sourceта отримує елементи, поки countелементи не виведені або sourceне містять більше елементів. Якщо countкількість елементів у них перевищує source, всі елементи sourceповертаються

Відмова:

Використання Пропустіть і Взяти всередині циклу означає, що перераховуються перераховуються кілька разів. Це небезпечно, якщо нумероване число відкладено. Це може спричинити багаторазове виконання запиту до бази даних, веб-запиту або зчитування файлу. Цей приклад явно стосується використання списку, який не відкладений, тому це менше проблем. Це все ще повільне рішення, оскільки пропуск перераховує колекцію кожного разу, коли вона викликається.

Це також можна вирішити за допомогою GetRangeметоду, але для вилучення можливої ​​групи спокою потрібен додатковий розрахунок:

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

Ось третій спосіб впоратися з цим, який працює з 2-х петель. Це гарантує, що колекція перераховується лише 1 раз !:

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

2
Дуже приємне рішення. Люди забули, як користуватися циклом
VitalickS

1
Використання Skipі Takeвсередині циклу означає, що перераховуються перераховуються кілька разів. Це небезпечно, якщо нумероване число відкладено. Це може спричинити багаторазове виконання запиту до бази даних, веб-запиту або зчитування файлу. У вашому прикладі ви маєте значення, Listяке не відкладається, тому це менше проблем.
Теодор Зуліяс

@TheodorZoulias так, я знаю, саме тому я сьогодні розмістив друге рішення. Я опублікував ваш коментар як відмову, тому що ви досить добре сформулювали це, я вам цитую?
Монг Чжу

Я написав третє рішення з 2-х циклів, щоб колекція перераховувалася лише 1 раз. річ skip.take - це дуже неефективне рішення
Монг Чжу

4

Той самий підхід, що і MoreLINQ, але використовуючи List замість Array. Я не робив бенчмаркінгу, але читабельність важлива для деяких людей:

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

1
НЕ слід повторно використовувати пакетну змінну. Ваші споживачі можуть бути повністю перекручені цим. Крім того, передайте sizeпараметр вашому, new Listщоб оптимізувати його розмір.
ErikE

1
Просте виправлення: замініть batch.Clear();наbatch = new List<T>();
NetMage

3

Ось спроба вдосконалення лінивих реалізацій Ніка Уолі ​​( посилання ) та infogulch ( посилання ) Batch. Цей суворий. Ви або перераховуєте партії у правильному порядку, або отримуєте виняток.

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

І ось лінива Batchреалізація джерел типу IList<T>. Цей не обмежує перерахування. Партії можна перераховувати частково, у будь-якому порядку та не раз. Однак обмеження щодо неможливості зміни колекції під час перерахування все ще діє. Це досягається шляхом виклику фіктивного дзвінка, enumerator.MoveNext()перш ніж отримати будь-який фрагмент або елемент. Мінусом є те, що нумератор залишається нерозкритим, оскільки невідомо, коли перерахування закінчиться.

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

2

Я приєднуюся до цього дуже пізно, але знайшов щось більш цікаве.

Тож ми можемо використовувати тут Skipі Takeдля кращої роботи.

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

    }

Далі я перевірив 100000 записів. Цикл лише займає більше часу у випадкуBatch

Код програми консолі.

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

Затрачений час такий.

Перший - 00: 00: 00.0708, 00: 00: 00.0660

Другий (Візьміть і пропустіть один) - 00: 00: 00.0008, 00: 00: 00.0008


1
GroupByповністю перераховується до того, як виробляє один ряд. Це не гарний спосіб зробити дозування.
ErikE

@ErikE Це залежить від того, що ти намагаєшся досягти. Якщо дозування не є проблемою, і вам просто потрібно розділити елементи на менші шматки для обробки, це може бути просто річ. Я використовую це для MSCRM, де може бути 100 записів, що для LAMBDA не
створює

1
Звичайно, є випадки використання, коли повне перерахування не має значення. Але навіщо писати корисний метод другого класу, коли можна написати чудовий?
ЕрікЕ

Хороша альтернатива, але не тотожна, як спочатку повертає список списків, що дозволяє вам пройти цикл.
Гарет Хопкінс

змінити , foreach (var batch in Ids2.Batch(5000))щоб var gourpBatch = Ids2.Batch(5000)і перевірити синхронізовані результати. або додати до списку var SecBatch = Ids2.Batch2(StartIndex, BatchSize);я був би зацікавлений, якщо ваші результати зміни часу змінитимуться.
Seabizkit

2

Отож, у функціональній шапці це виглядає тривіально .... але у C # є деякі суттєві недоліки.

ви, мабуть, розглядаєте це як розгортання IEnumerable (google це, і ви, ймовірно, опинитесь в деяких документах Haskell, але, можливо, є деякі F # речі, використовуючи розгортання, якщо ви знаєте F #, прижмуріться до документів Haskell, і це зробить сенс).

Розгортання пов'язане зі складанням ("сукупністю"), за винятком того, а не з повторенням через вхідний IEnumerable, воно повторюється через структури вихідних даних (подібний взаємозв'язок між IEnumerable та IObservable, адже я думаю, що IObservable реалізує "розгорнутий" під назвою генерувати. ..)

у будь-якому випадку спочатку вам потрібен метод розгортання, я думаю, що це працює (на жаль, він врешті-решт підіб'є стек для великих "списків" ... ви можете сміливо писати це у F #, використовуючи дохід! а не 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)));
    }

це трохи тупо, тому що C # не реалізує деякі речі, які функціональні мови приймають як належне ... але в основному це бере насіння, а потім генерує "Можливо" відповідь наступного елемента в IEnumerable та наступного насіння (можливо не існує в C #, тому ми використовували IEnumerable, щоб підробити це), і поєднує решту відповідей (я не можу порушити складність "O (n?)" цього).

Після того, як ви це зробили тоді;

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

все виглядає досить чисто ... ви приймаєте елементи "n" як "наступний" елемент IEnumerable, а "хвіст" - це решта не обробленого списку.

якщо в голові нічого немає ... ви закінчилися ... ви повертаєте "Нічого" (але підроблений як порожній IEnumerable>) ... ще повертаєте головний елемент і хвіст для обробки.

ви, мабуть, можете це зробити за допомогою IObservable, там, мабуть, існує такий тип "Batch", і ви, ймовірно, можете це використовувати.

Якщо ризик стека переповнює занепокоєння (це, мабуть, повинно бути), тоді вам слід реалізувати у F # (а можливо, вже існує і якась бібліотека F # (FSharpX?)).

(Я робив лише деякі рудиментарні тести на це, тому там можуть бути дивні помилки).


1

Я написав власну IEnumerable реалізацію, яка працює без linq і гарантує єдине перерахування даних. Він також виконує все це, не вимагаючи резервних списків або масивів, які викликають вибухи пам'яті у великих наборах даних.

Ось кілька основних тестів:

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

Метод розширення для розділення даних.

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

Це клас реалізації

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

1

Я знаю, що всі використовували складні системи для цієї роботи, і я дійсно не розумію, чому. Зняти і пропустити дозволить усі ці операції, що використовують загальний вибір з Func<TSource,Int32,TResult>функцією перетворення. Подібно до:

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

2
Це може бути дуже неефективним, оскільки дані sourceбудуть повторюватися дуже часто.
Кевін Мейєр

1
Це не тільки неефективно, але може також дати невірні результати. Немає гарантії, що численні дадуть однакові елементи при перерахунку вдвічі. Візьміть цей перелічуваних в якості прикладу: Enumerable.Range(0, 1).SelectMany(_ => Enumerable.Range(0, new Random().Next())).
Теодор Зуліяс

1

Ще одна реалізація одного рядка. Це працює навіть з порожнім списком, в цьому випадку ви отримуєте колекцію партій нульового розміру.

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

1

Інший спосіб - використання оператора буфера Rx

//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(). Це запах коду для синхронного коду, який насильно викликає асинхронний код.
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);
    }

Додайте опис / текст у відповідь. Введення лише коду може означати менше часу.
Ariful Haque
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.