Разделить список на подсписки с LINQ

Есть ли способ разделить List<SomeObject> на несколько отдельных списков SomeObject, используя индекс элемента в качестве разделителя каждого разделения?

Позвольте мне проиллюстрировать:

У меня есть List<SomeObject>, и мне нужны List<List<SomeObject>> или List<SomeObject>[], чтобы каждый из этих результирующих списков содержал группу из 3 элементов исходного списка (последовательно).

например.:

  • Оригинальный список: [a, g, e, w, p, s, q, f, x, y, i, m, c]

  • Результирующие списки: [a, g, e], [w, p, s], [q, f, x], [y, i, m], [c]

Мне также нужно, чтобы размер результирующих списков был параметром этой функции.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
406
0
231 671
32
Перейти к ответу Данный вопрос помечен как решенный

Ответы 32

Если список имеет тип system.collections.generic, вы можете использовать метод CopyTo, доступный для копирования элементов вашего массива в другие подмассивы. Вы указываете начальный элемент и количество элементов для копирования.

Вы также можете сделать 3 копии исходного списка и использовать RemoveRange в каждом списке, чтобы уменьшить список до нужного размера.

Или просто создайте вспомогательный метод, который сделает это за вас.

Вы мог используете несколько запросов, которые используют Take и Skip, но я считаю, что это добавит слишком много итераций в исходный список.

Скорее, я думаю, вам следует создать собственный итератор, например:

public static IEnumerable<IEnumerable<T>> GetEnumerableOfEnumerables<T>(
  IEnumerable<T> enumerable, int groupSize)
{
   // The list to return.
   List<T> list = new List<T>(groupSize);

   // Cycle through all of the items.
   foreach (T item in enumerable)
   {
     // Add the item.
     list.Add(item);

     // If the list has the number of elements, return that.
     if (list.Count == groupSize)
     {
       // Return the list.
       yield return list;

       // Set the list to a new list.
       list = new List<T>(groupSize);
     }
   }

   // Return the remainder if there is any,
   if (list.Count != 0)
   {
     // Return the list.
     yield return list;
   }
}

Затем вы можете вызвать это, и он будет включен LINQ, чтобы вы могли выполнять другие операции с результирующими последовательностями.


В свете Ответ Сэма я почувствовал, что есть более простой способ сделать это без:

  • Повторный просмотр списка (чего я изначально не делал)
  • Материализация элементов в группах перед освобождением блока (для больших блоков элементов могут возникнуть проблемы с памятью)
  • Весь код, который опубликовал Сэм

Тем не менее, вот еще один проход, который я кодифицировал в методе расширения для IEnumerable<T> под названием Chunk:

public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, 
    int chunkSize)
{
    // Validate parameters.
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (chunkSize <= 0) throw new ArgumentOutOfRangeException(nameof(chunkSize),
        "The chunkSize parameter must be a positive value.");

    // Call the internal implementation.
    return source.ChunkInternal(chunkSize);
}

Ничего удивительного, просто проверка основных ошибок.

Переходим к ChunkInternal:

private static IEnumerable<IEnumerable<T>> ChunkInternal<T>(
    this IEnumerable<T> source, int chunkSize)
{
    // Validate parameters.
    Debug.Assert(source != null);
    Debug.Assert(chunkSize > 0);

    // Get the enumerator.  Dispose of when done.
    using (IEnumerator<T> enumerator = source.GetEnumerator())
    do
    {
        // Move to the next element.  If there's nothing left
        // then get out.
        if (!enumerator.MoveNext()) yield break;

        // Return the chunked sequence.
        yield return ChunkSequence(enumerator, chunkSize);
    } while (true);
}

Обычно он получает IEnumerator<T> и вручную выполняет итерацию по каждому элементу. Он проверяет, есть ли в данный момент какие-либо элементы для перечисления. После того, как каждый кусок пронумерован, если не осталось никаких элементов, он разбивается.

Как только он обнаруживает, что в последовательности есть элементы, он делегирует ответственность за внутреннюю реализацию IEnumerable<T> на ChunkSequence:

private static IEnumerable<T> ChunkSequence<T>(IEnumerator<T> enumerator, 
    int chunkSize)
{
    // Validate parameters.
    Debug.Assert(enumerator != null);
    Debug.Assert(chunkSize > 0);

    // The count.
    int count = 0;

    // There is at least one item.  Yield and then continue.
    do
    {
        // Yield the item.
        yield return enumerator.Current;
    } while (++count < chunkSize && enumerator.MoveNext());
}

Поскольку MoveNext уже был вызван на IEnumerator<T>, переданном в ChunkSequence, он возвращает элемент, возвращаемый Current, а затем увеличивает счет, следя за тем, чтобы никогда не возвращалось больше элементов chunkSize, и переходя к следующему элементу в последовательности после каждой итерации (но коротко- цепляется, если количество полученных элементов превышает размер блока).

Если элементов не осталось, метод InternalChunk выполнит еще один проход во внешнем цикле, но при повторном вызове MoveNext он все равно вернет false, согласно документации (выделено мной):

If MoveNext passes the end of the collection, the enumerator is positioned after the last element in the collection and MoveNext returns false. When the enumerator is at this position, subsequent calls to MoveNext also return false until Reset is called.

На этом этапе цикл прерывается, а последовательность последовательностей завершается.

Это простой тест:

static void Main()
{
    string s = "agewpsqfxyimc";

    int count = 0;

    // Group by three.
    foreach (IEnumerable<char> g in s.Chunk(3))
    {
        // Print out the group.
        Console.Write("Group: {0} - ", ++count);

        // Print the items.
        foreach (char c in g)
        {
            // Print the item.
            Console.Write(c + ", ");
        }

        // Finish the line.
        Console.WriteLine();
    }
}

Выход:

Group: 1 - a, g, e,
Group: 2 - w, p, s,
Group: 3 - q, f, x,
Group: 4 - y, i, m,
Group: 5 - c,

Важное замечание: нет будет работать, если вы не слейте всю дочернюю последовательность или не прервете какой-либо момент в родительской последовательности. Это важное предостережение, но если ваш вариант использования заключается в том, что вы будете использовать элемент каждый последовательности последовательностей, тогда это сработает для вас.

Кроме того, он будет делать странные вещи, если вы играете с порядком, точно так же, как Сэм сделал в какой-то момент.

Я думаю, что это лучшее решение ... единственная проблема в том, что у списка нет Length ... у него есть Count. Но это легко изменить. Мы можем сделать это лучше, даже не создавая списки, а возвращая ienumerables, которые содержат ссылки на основной список с комбинацией смещения / длины. Итак, если размер группы большой, мы не тратим впустую память. Прокомментируйте, если хотите, чтобы я это написал.

Amir 18.11.2009 07:39

@ Амир, я бы хотел, чтобы это было написано

samandmoore 31.05.2011 19:38

Это красиво и быстро - Кэмерон опубликовал очень похожий, после вашего, с той лишь оговоркой, что он буферизует куски, это может привести к нехватке памяти, если куски и размеры элементов большие. Обратитесь к моему ответу за альтернативным, хотя и более сложным ответом.

Sam Saffron 03.05.2012 15:59

@SamSaffron Да, если у вас есть большое количество элементов в List<T>, у вас, очевидно, будут проблемы с памятью из-за буферизации. Оглядываясь назад, я должен был отметить это в ответе, но в то время казалось, что основное внимание уделялось слишком большому количеству итераций. Тем не менее, ваше решение действительно более сложное. Я не тестировал его, но теперь мне интересно, есть ли менее сложное решение.

casperOne 03.05.2012 16:05

@casperOne, да ... Google дал мне эту страницу, когда я искал способ разделить перечислимые объекты, для моего конкретного случая использования я разбиваю безумно большой список записей, которые возвращаются из базы данных, если я материализую их в список, который он взорвет (на самом деле у dapper есть опция buffer: false только для этого варианта использования)

Sam Saffron 03.05.2012 16:11

любопытно увидеть более чистое решение, которое поддерживает ту же производительность, это очень интересная проблема

Sam Saffron 03.05.2012 16:13

@ SamSaffron Я займусь этим сегодня. Вам нужна потокобезопасность? Я столкнулся с этой проблемой при переносе вещей из базы данных очень давно. Разбиение на части в базе данных (если ваши таблицы позволяют это) очень помогает (у меня есть сверхглубокая таблица с сотнями миллионов строк, на которых у меня есть естественные разделы, поэтому я могу разбивать на части на уровне базы данных, а запросы не сходят с ума).

casperOne 03.05.2012 16:20

Безопасность потоков на самом деле не является проблемой, которую мне нужно решать, мое решение было бы легко адаптировать к потокобезопасному довольно легко, но многопоточность, вероятно, немного ухудшит производительность, если не будут внесены какие-либо другие настройки, эта проблема показалась мне довольно интересной, что является почему я потратил на это час или два.

Sam Saffron 03.05.2012 16:28

изначально я передавал счетчики, и моя реализация была намного проще, но Enumerable.Range(0, 100).Chunk(3).Reverse().ToArray() было действительно сложно исправить

Sam Saffron 04.05.2012 01:52

Изначально мне нужна была безопасность потоков, так как мой запрос linq был context.Select (GetJobData) .Clump (10) .AsParallel (). Select (Pro‌ cessJobs), и мне нужно было сгруппировать задания, чтобы поток работал в течение разумного периода времени. вместо массового переключения потоков. :П

Cameron MacFarland 05.05.2012 06:13

@Sam Saffron: «Enumerable.Range (0, 100) .Chunk (3) .Reverse (). ToArray ()» можно исправить простым: «Enumerable.Range (0, 100) .Chunk (3) .Select (e => e.ToList ()). Reverse (). ToArray () "до тех пор, пока вы работаете с небольшими наборами данных ... Поскольку он, очевидно, материализует все данные в памяти заранее.

Jens 25.10.2013 01:07

@Sam Saffron: Если вам нужно что-то для больших наборов данных из базы данных, я бы подумал, что нужно глубоко изучить деревья выражений (например, LINQ to SQL, если это база данных такого типа) и каким-то образом найти решение, которое позволило бы каждому «фрагменту» или «Раздел» должен быть отображен под ним ... То есть офс. не простые вещи ^^

Jens 25.10.2013 01:11

Это явный победитель с точки зрения компромисса между эффективностью и простотой. Слишком много комментариев :)

Ohad Schneider 24.08.2017 11:54
Ответ принят как подходящий

Попробуйте следующий код.

public static IList<IList<T>> Split<T>(IList<T> source)
{
    return  source
        .Select((x, i) => new { Index = i, Value = x })
        .GroupBy(x => x.Index / 3)
        .Select(x => x.Select(v => v.Value).ToList())
        .ToList();
}

Идея состоит в том, чтобы сначала сгруппировать элементы по индексам. Разделение на три приводит к группированию их в группы по 3. Затем каждую группу преобразуют в список, а IEnumerable из List в List из List.

GroupBy выполняет неявную сортировку. Это может убить производительность. Нам нужна какая-то инверсия SelectMany.

yfeldblum 07.01.2009 06:19

@Justice, было бы неплохо иметь встроенную систему разделения для IEnumerable.

JaredPar 07.01.2009 06:31

@JaredPar Вы можете сделать это достаточно легко с помощью метода расширения. Я подозреваю, что его там нет, отчасти потому, что он плохо интегрируется с SQL. 'myEnumerable.InGroupsOf (3) .Select (subEnumerable => subEnumerable.Sum ()). Average ()' плюс перегрузки было бы неплохо.

yfeldblum 07.01.2009 07:17

@Justice, GroupBy может быть реализовано с помощью хеширования. Откуда вы знаете, что реализация GroupBy «может убить производительность»?

Amy B 07.01.2009 17:43

GroupBy ничего не возвращает, пока не перечислит все элементы. Вот почему это медленно. Списки, которые хочет OP, являются смежными, поэтому лучший метод мог бы выдать первый подсписок [a,g,e] перед перечислением каких-либо дополнительных элементов исходного списка.

Colonel Panic 11.07.2012 18:20

Возьмем крайний пример бесконечного IEnumerable. GroupBy(x=>f(x)).First() никогда не уступит группу. OP спросил о списках, но если мы напишем для работы с IEnumerable, сделав только одну итерацию, мы получим преимущество в производительности.

Colonel Panic 12.07.2012 02:16

Стоит отметить, что .GroupBy(x => x.Index % 3) разделит всю коллекцию поровну на 3 части, поэтому, если у вас есть 30 элементов, вы получите 3 списка из 10. Текущий пример дает вам 10 списков из 3, если у вас их 30.

The Muffin Man 27.08.2013 23:12

@Nick Order не сохранился для вас. Это все еще полезно знать, но вы бы сгруппировали их в (0,3,6,9, ...), (1,4,7,10, ...), (2,5,8 , 11, ...). Если порядок не имеет значения, тогда все в порядке, но в данном случае кажется, что это имеет значение.

Reafexus 05.09.2013 23:52

Проверьте использование MoreLinq в этом посте: stackoverflow.com/questions/13731796/create-batches-in-linq

pasx 10.03.2015 02:19

Разве этот простой код int i=0; return source.GroupBy(x => (i++/3)).ToList() не работал бы? Он отлично работает для меня.

Jihad Haddad 26.11.2015 14:35

Не удается преобразовать общий список <list <t>> в общий ilist <ilist <t>>?

Jjang 25.01.2016 10:04

заменить IList<IList<T>> Split<T>(IList<T> source) на IList<List<T>> Split<T>(IList<T> source)

Kai 23.02.2016 09:54

У меня был хороший успех при использовании .GroupBy(x => Math.Round(x.Index / chunkSize))

kevp 05.04.2016 23:50

на последнем .ToList();: Ошибка 45 Невозможно неявно преобразовать тип System.Collections.Generic.List<System.Collections.Generic.L‌​ist<T>> в System.Collections.Generic.IList<System.Collections.Generic.‌​IList<T>>. Существует явное преобразование (вам не хватает приведения?)

Serge 19.04.2016 17:02

@yfeldblum, пожалуйста, посмотрите мой ответ без группы stackoverflow.com/a/53532961/3360759

frhack 29.11.2018 09:22

Вот процедура разделения списка, которую я написал пару месяцев назад:

public static List<List<T>> Chunk<T>(
    List<T> theList,
    int chunkSize
)
{
    List<List<T>> result = theList
        .Select((x, i) => new {
            data = x,
            indexgroup = i / chunkSize
        })
        .GroupBy(x => x.indexgroup, x => x.data)
        .Select(g => new List<T>(g))
        .ToList();

    return result;
}

Мы обнаружили, что решение Дэвида Б. работает лучше всего. Но мы адаптировали его к более общему решению:

list.GroupBy(item => item.SomeProperty) 
   .Select(group => new List<T>(group)) 
   .ToArray();

Это хорошо, но сильно отличается от того, о чем просил исходный вопрос.

Amy B 27.06.2011 18:50

Я только что написал это и думаю, что это немного элегантнее, чем другие предлагаемые решения:

/// <summary>
/// Break a list of items into chunks of a specific size
/// </summary>
public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize)
{
    while (source.Any())
    {
        yield return source.Take(chunksize);
        source = source.Skip(chunksize);
    }
}

Люблю это решение. Я бы рекомендовал добавить эту проверку работоспособности, чтобы предотвратить бесконечный цикл: if (chunksize <= 0) throw new ArgumentException("Chunk size must be greater than zero.", "chunksize");

mroach 01.03.2012 20:34

Мне это нравится, но это не очень эффективно

Sam Saffron 03.05.2012 08:26

Я реализовал свой метод фрагментов IQueryable <> таким образом, за исключением того, что я сделал Count (), чтобы избежать n-1 .Any (). Я предполагаю, что оптимальная реализация зависит от контекста для IQueryables (в моем случае попадание в базу данных с LINQ to SQL).

Guillaume86 03.05.2012 20:51

Для последовательности из 13 элементов с размером блока 3 этот алгоритм будет обращаться к элементам 91 раз. Это не проблема для маленьких последовательностей, но очень неэффективно для больших.

Martin Liversage 10.08.2012 14:28

Мне нравится этот, но эффективность по времени - O(n²). Вы можете просмотреть список и получить время O(n).

hIpPy 30.08.2012 22:22

@hIpPy, как это n ^ 2? Мне кажется линейным

Vivek Maharajh 10.10.2013 01:02

@vivekmaharajh source каждый раз заменяется упакованным IEnumerable. Таким образом, взятие элементов из source проходит через слои Skip.

Lasse Espeholt 24.11.2013 16:04

@hIpPy Это действительно будет O (n ^ 2) для IEnumerable с задержкой. Однако, если у вас есть старый добрый array[] или List, это будет O(n). Если вам нравится это решение и у вас есть IEnumerable, вы можете использовать .ToArray(), прежде чем передавать его в Chunk, и получить O(n). Конечно, недостатками являются дополнительная память и потенциальная дополнительная производительность (если у вас есть вызывающие абоненты, которые могут не перечислять все фрагменты, вы излишне перечисляете все source). Из-за этих недостатков ручное выполнение итерации самостоятельно, как предлагает @hlpPy, вероятно, является лучшим вариантом для производственного кода.

Vivek Maharajh 04.12.2013 11:31

@vivekmaharajh: как вы думаете, почему этот список или массив делает его 0(n), они не оптимизированы, а Skip и Take всегда будут перечислять последовательность до этого момента, поэтому имеет сложность O(n^2). В очень большом списке аппорации Skip / Take бесполезны. Я бы использовал GroupBy с целочисленным делением или (более эффективно) Batch от LINQ.

Tim Schmelter 11.12.2014 15:15

Вы не можете получить O (n) по простой причине: вы можете перебирать внешний и внутренний счетчики в случайном порядке. Единственное улучшение этого подхода - объединение Any () и Take ().

realbart 22.07.2016 15:06

Супер элегантный ответ.

Regianni 09.03.2021 13:33

В целом подход, предложенный CaseyB, работает нормально, на самом деле, если вы передаете List<T>, его трудно обвинить, возможно, я бы изменил его на:

public static IEnumerable<IEnumerable<T>> ChunkTrivialBetter<T>(this IEnumerable<T> source, int chunksize)
{
   var pos = 0; 
   while (source.Skip(pos).Any())
   {
      yield return source.Skip(pos).Take(chunksize);
      pos += chunksize;
   }
}

Это позволит избежать массовых цепочек вызовов. Тем не менее, у этого подхода есть общий недостаток. Он материализует два перечисления для каждого фрагмента, чтобы выделить проблему, которую нужно выполнить:

foreach (var item in Enumerable.Range(1, int.MaxValue).Chunk(8).Skip(100000).First())
{
   Console.WriteLine(item);
}
// wait forever 

Чтобы преодолеть это, мы можем попробовать подход Кэмерона, который успешно проходит вышеуказанный тест, поскольку он проходит через перечисление только один раз.

Проблема в том, что у него есть другой недостаток, он материализует каждый элемент в каждом фрагменте, проблема с этим подходом заключается в том, что у вас много памяти.

Чтобы проиллюстрировать это, попробуйте запустить:

foreach (var item in Enumerable.Range(1, int.MaxValue)
               .Select(x => x + new string('x', 100000))
               .Clump(10000).Skip(100).First())
{
   Console.Write('.');
}
// OutOfMemoryException

Наконец, любая реализация должна иметь возможность обрабатывать неупорядоченную итерацию фрагментов, например:

Enumerable.Range(1,3).Chunk(2).Reverse().ToArray()
// should return [3],[1,2]

Многие высокооптимальные решения, такие как мой первый пересмотр этого ответа, не дали результата. Ту же проблему можно увидеть в ответе casperOne оптимизирован.

Для решения всех этих проблем вы можете использовать следующее:

namespace ChunkedEnumerator
{
    public static class Extensions 
    {
        class ChunkedEnumerable<T> : IEnumerable<T>
        {
            class ChildEnumerator : IEnumerator<T>
            {
                ChunkedEnumerable<T> parent;
                int position;
                bool done = false;
                T current;


                public ChildEnumerator(ChunkedEnumerable<T> parent)
                {
                    this.parent = parent;
                    position = -1;
                    parent.wrapper.AddRef();
                }

                public T Current
                {
                    get
                    {
                        if (position == -1 || done)
                        {
                            throw new InvalidOperationException();
                        }
                        return current;

                    }
                }

                public void Dispose()
                {
                    if (!done)
                    {
                        done = true;
                        parent.wrapper.RemoveRef();
                    }
                }

                object System.Collections.IEnumerator.Current
                {
                    get { return Current; }
                }

                public bool MoveNext()
                {
                    position++;

                    if (position + 1 > parent.chunkSize)
                    {
                        done = true;
                    }

                    if (!done)
                    {
                        done = !parent.wrapper.Get(position + parent.start, out current);
                    }

                    return !done;

                }

                public void Reset()
                {
                    // per http://msdn.microsoft.com/en-us/library/system.collections.ienumerator.reset.aspx
                    throw new NotSupportedException();
                }
            }

            EnumeratorWrapper<T> wrapper;
            int chunkSize;
            int start;

            public ChunkedEnumerable(EnumeratorWrapper<T> wrapper, int chunkSize, int start)
            {
                this.wrapper = wrapper;
                this.chunkSize = chunkSize;
                this.start = start;
            }

            public IEnumerator<T> GetEnumerator()
            {
                return new ChildEnumerator(this);
            }

            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }

        }

        class EnumeratorWrapper<T>
        {
            public EnumeratorWrapper (IEnumerable<T> source)
            {
                SourceEumerable = source;
            }
            IEnumerable<T> SourceEumerable {get; set;}

            Enumeration currentEnumeration;

            class Enumeration
            {
                public IEnumerator<T> Source { get; set; }
                public int Position { get; set; }
                public bool AtEnd { get; set; }
            }

            public bool Get(int pos, out T item) 
            {

                if (currentEnumeration != null && currentEnumeration.Position > pos)
                {
                    currentEnumeration.Source.Dispose();
                    currentEnumeration = null;
                }

                if (currentEnumeration == null)
                {
                    currentEnumeration = new Enumeration { Position = -1, Source = SourceEumerable.GetEnumerator(), AtEnd = false };
                }

                item = default(T);
                if (currentEnumeration.AtEnd)
                {
                    return false;
                }

                while(currentEnumeration.Position < pos) 
                {
                    currentEnumeration.AtEnd = !currentEnumeration.Source.MoveNext();
                    currentEnumeration.Position++;

                    if (currentEnumeration.AtEnd) 
                    {
                        return false;
                    }

                }

                item = currentEnumeration.Source.Current;

                return true;
            }

            int refs = 0;

            // needed for dispose semantics 
            public void AddRef()
            {
                refs++;
            }

            public void RemoveRef()
            {
                refs--;
                if (refs == 0 && currentEnumeration != null)
                {
                    var copy = currentEnumeration;
                    currentEnumeration = null;
                    copy.Source.Dispose();
                }
            }
        }

        public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize)
        {
            if (chunksize < 1) throw new InvalidOperationException();

            var wrapper =  new EnumeratorWrapper<T>(source);

            int currentPos = 0;
            T ignore;
            try
            {
                wrapper.AddRef();
                while (wrapper.Get(currentPos, out ignore))
                {
                    yield return new ChunkedEnumerable<T>(wrapper, chunksize, currentPos);
                    currentPos += chunksize;
                }
            }
            finally
            {
                wrapper.RemoveRef();
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            int i = 10;
            foreach (var group in Enumerable.Range(1, int.MaxValue).Skip(10000000).Chunk(3))
            {
                foreach (var n in group)
                {
                    Console.Write(n);
                    Console.Write(" ");
                }
                Console.WriteLine();
                if (i-- == 0) break;
            }


            var stuffs = Enumerable.Range(1, 10).Chunk(2).ToArray();

            foreach (var idx in new [] {3,2,1})
            {
                Console.Write("idx " + idx + " ");
                foreach (var n in stuffs[idx])
                {
                    Console.Write(n);
                    Console.Write(" ");
                }
                Console.WriteLine();
            }

            /*

10000001 10000002 10000003
10000004 10000005 10000006
10000007 10000008 10000009
10000010 10000011 10000012
10000013 10000014 10000015
10000016 10000017 10000018
10000019 10000020 10000021
10000022 10000023 10000024
10000025 10000026 10000027
10000028 10000029 10000030
10000031 10000032 10000033
idx 3 7 8
idx 2 5 6
idx 1 3 4
             */

            Console.ReadKey();


        }

    }
}

Существует также ряд оптимизаций, которые вы можете ввести для итерации фрагментов вне очереди, которая здесь выходит за рамки.

Какой метод выбрать? Это полностью зависит от проблемы, которую вы пытаетесь решить. Если вас не беспокоит первый недостаток, простой ответ будет невероятно привлекательным.

Примечание, как и большинство методов, это небезопасно для многопоточности, все может стать странным, если вы хотите сделать его потокобезопасным, вам нужно будет изменить EnumeratorWrapper.

Будет ли ошибка Enumerable.Range (0, 100) .Chunk (3) .Reverse (). ToArray () неправильной, или Enumerable.Range (0, 100) .ToArray (). Chunk (3) .Reverse () .ToArray () генерирует исключение?

Cameron MacFarland 03.05.2012 10:39

@SamSaffron Я обновил свой ответ и значительно упростил код для того, что я считаю наиболее распространенным вариантом использования (и признаю предостережения).

casperOne 03.05.2012 19:45

А как насчет разбиения на части IQueryable <>? Я предполагаю, что подход Take / Skip будет оптимальным, если мы хотим делегировать максимум операций поставщику.

Guillaume86 03.05.2012 20:45

@ Guillaume86 Я согласен, если у вас есть IList или IQueryable, вы можете использовать всевозможные ярлыки, которые сделают это намного быстрее (Linq делает это внутренне для всех видов других методов)

Sam Saffron 04.05.2012 01:53

Это, безусловно, лучший ответ по эффективности. У меня проблема с использованием SqlBulkCopy с IEnumerable, который запускает дополнительные процессы в каждом столбце, поэтому он должен эффективно выполняться только за один проход. Это позволит мне разбить IEnumerable на куски управляемого размера. (Для тех, кому интересно, я включил потоковый режим SqlBulkCopy, который, похоже, не работает).

Brain2000 30.03.2016 22:23

Это сократило простой метод linq с 6 секунд до нуля. Действительно быстро!

Jerther 11.08.2016 18:18

Создавая свою собственную версию, не глядя на вашу, я думаю, что Select мог бы быть намного более эффективным, если бы он пропустил вызов функции selector, если не вызывается Current (по общему признанию, вызывая игнорирование побочных эффектов в selector - возможно, selector можно было бы оценить на предмет ссылок без параметров) .

NetMage 02.05.2018 21:37

Для этой цели System.Interactive предоставляет Buffer(). Некоторые быстрые тесты показывают, что производительность аналогична решению Сэма.

вы знаете семантику буферизации? например: если у вас есть счетчик, который выдает строки размером 300 КБ и пытается разбить их на блоки размером 10 000, вы получите нехватку памяти?

Sam Saffron 03.05.2012 15:52
Buffer() возвращает IEnumerable<IList<T>>, так что да, у вас, вероятно, возникнут проблемы - он не транслируется, как ваш.
dahlbyk 03.05.2012 23:53

Несколько лет назад я написал метод расширения Clump. Отлично работает, и это самая быстрая реализация здесь. :П

/// <summary>
/// Clumps items into same size lots.
/// </summary>
/// <typeparam name = "T"></typeparam>
/// <param name = "source">The source list of items.</param>
/// <param name = "size">The maximum size of the clumps to make.</param>
/// <returns>A list of list of items, where each list of items is no bigger than the size given.</returns>
public static IEnumerable<IEnumerable<T>> Clump<T>(this IEnumerable<T> source, int size)
{
    if (source == null)
        throw new ArgumentNullException("source");
    if (size < 1)
        throw new ArgumentOutOfRangeException("size", "size must be greater than 0");

    return ClumpIterator<T>(source, size);
}

private static IEnumerable<IEnumerable<T>> ClumpIterator<T>(IEnumerable<T> source, int size)
{
    Debug.Assert(source != null, "source is null.");

    T[] items = new T[size];
    int count = 0;
    foreach (var item in source)
    {
        items[count] = item;
        count++;

        if (count == size)
        {
            yield return items;
            items = new T[size];
            count = 0;
        }
    }
    if (count > 0)
    {
        if (count == size)
            yield return items;
        else
        {
            T[] tempItems = new T[count];
            Array.Copy(items, tempItems, count);
            yield return tempItems;
        }
    }
}

он должен работать, но он буферизует 100% кусков, я пытался этого избежать ... но это оказалось невероятно опасным.

Sam Saffron 03.05.2012 11:03

@SamSaffron Ага. Особенно, если вы добавите в микс такие вещи, как plinq, для чего изначально была моя реализация.

Cameron MacFarland 03.05.2012 11:06

расширил свой ответ, дайте мне знать, что вы думаете

Sam Saffron 05.05.2012 04:59

@CameronMacFarland - можете ли вы объяснить, почему необходима вторая проверка count == size? Спасибо.

dugas 26.09.2013 22:44

Мы можем улучшить решение @JaredPar, чтобы выполнять истинную ленивую оценку. Мы используем метод GroupAdjacentBy, который дает группы последовательных элементов с одним и тем же ключом:

sequence
.Select((x, i) => new { Value = x, Index = i })
.GroupAdjacentBy(x=>x.Index/3)
.Select(g=>g.Select(x=>x.Value))

Поскольку группы создаются по одной, это решение эффективно работает с длинными или бесконечными последовательностями.

Использование модульного разбиения:

public IEnumerable<IEnumerable<string>> Split(IEnumerable<string> input, int chunkSize)
{
    var chunks = (int)Math.Ceiling((double)input.Count() / (double)chunkSize);
    return Enumerable.Range(0, chunks).Select(id => input.Where(s => s.GetHashCode() % chunks == id));
}

Что насчет этого?

var input = new List<string> { "a", "g", "e", "w", "p", "s", "q", "f", "x", "y", "i", "m", "c" };
var k = 3

var res = Enumerable.Range(0, (input.Count - 1) / k + 1)
                    .Select(i => input.GetRange(i * k, Math.Min(k, input.Count - i * k)))
                    .ToList();

Насколько мне известно, GetRange () линейно по количеству взятых предметов. Так что это должно работать хорошо.

Хорошо, вот мое мнение:

  • полностью ленив: работает с бесконечными перечислениями
  • без промежуточного копирования / буферизации
  • O (n) время выполнения
  • работает также, когда внутренние последовательности используются только частично

public static IEnumerable<IEnumerable<T>> Chunks<T>(this IEnumerable<T> enumerable,
                                                    int chunkSize)
{
    if (chunkSize < 1) throw new ArgumentException("chunkSize must be positive");

    using (var e = enumerable.GetEnumerator())
    while (e.MoveNext())
    {
        var remaining = chunkSize;    // elements remaining in the current chunk
        var innerMoveNext = new Func<bool>(() => --remaining > 0 && e.MoveNext());

        yield return e.GetChunk(innerMoveNext);
        while (innerMoveNext()) {/* discard elements skipped by inner iterator */}
    }
}

private static IEnumerable<T> GetChunk<T>(this IEnumerator<T> e,
                                          Func<bool> innerMoveNext)
{
    do yield return e.Current;
    while (innerMoveNext());
}

Пример использования

var src = new [] {1, 2, 3, 4, 5, 6}; 

var c3 = src.Chunks(3);      // {{1, 2, 3}, {4, 5, 6}}; 
var c4 = src.Chunks(4);      // {{1, 2, 3, 4}, {5, 6}}; 

var sum   = c3.Select(c => c.Sum());    // {6, 15}
var count = c3.Count();                 // 2
var take2 = c3.Select(c => c.Take(2));  // {{1, 2}, {4, 5}}

Пояснения

Код работает путем вложения двух итераторов на основе yield.

Внешний итератор должен отслеживать, сколько элементов было эффективно использовано внутренним (фрагментным) итератором. Это делается путем закрытия remaining с помощью innerMoveNext(). Непотребленные элементы блока отбрасываются до того, как внешний итератор выдаст следующий блок. Это необходимо, потому что в противном случае вы получите противоречивые результаты, когда внутренние перечисления не будут (полностью) потреблены (например, c3.Count() вернет 6).

Note:The answer has been updated to address the shortcomings pointed out by @aolszowka.

Очень хорошо. Мое «правильное» решение было намного сложнее. Это ответ №1 ИМХО.

CaseyB 09.01.2014 01:37

Это страдает неожиданным (с точки зрения API) поведением при вызове ToArray (), а также не является потокобезопасным.

aolszowka 13.05.2014 17:57

@aolszowka: не могли бы вы уточнить?

3dGrabber 14.05.2014 12:40

@ 3dGrabber Возможно, именно так я реорганизовал ваш код (извините, это слишком долго, чтобы пройти здесь, в основном вместо метода расширения, который я передал в sourceEnumerator). Тестовый пример, который я использовал, был похож на этот эффект: int [] arrayToSort = new int [] {9, 7, 2, 6, 3, 4, 8, 5, 1, 10, 11, 12, 13}; var source = Chunkify <int> (arrayToSort, 3) .ToArray (); В результате в Source указано, что было 13 блоков (количество элементов). Для меня это имело смысл, поскольку, если вы не запросили внутренние перечисления, Enumerator не увеличивался.

aolszowka 14.05.2014 18:40

@ 3dGrabber (продолжение) Кроме того, поскольку внутренние перечисления не генерируются до тех пор, пока к ним не обращаются, попытка выполнить какой-либо тип поточной операции над ними может привести к состоянию гонки, в котором поток вызывает sourceEnumerator.MoveNext () до того, как другой поток читает sourceEnumerator. Текущий. По крайней мере, это то, что я придумал, если я ошибаюсь, дайте мне знать, я хочу учиться!

aolszowka 14.05.2014 18:49

@aolszowka: очень важные моменты. Я добавил предупреждение и раздел использования. Код предполагает, что вы перебираете внутреннее перечислимое. Однако с вашим решением вы избавляетесь от лени. Я думаю, что можно получить лучшее из обоих миров с помощью настраиваемого кеширующего IEnumerator. Если найду решение, выложу здесь ...

3dGrabber 20.05.2014 13:10

yield return enumerator.GetChunk (chunkSize) .ToArray (); это простое решение за счет буферизации. Другой вариант - выбросить исключение, когда предыдущее внутреннее перечислимое не исчерпано, когда в следующий раз будет возвращено внешнее. (Я подозреваю, что вы думаете о буферизации только в случае необходимости; это тоже было бы круто.)

CaseyB 21.05.2014 18:10

@ 3dGrabber Я пытаюсь использовать это (потому что элегантно) для неленивого случая, чтобы разделить большие коллекции сложных объектов (в основном, get и .ToList ()), но не могу заставить его вернуть больше, чем первый кусок . Нет специального перечислителя. Понимая, что это расплывчато, есть идеи, почему это может произойти с прямой (не универсальной) копией этого?

downwitch 26.07.2016 21:24

@ 3dGrabber ничего, извините за беспокойство. В моем случае я расширил innerMoveNext для ведения журнала и смог увидеть, что проблема была вызвана вызовом while (innerMoveNext()) {/* discard elements skipped by inner iterator */} - как только я удалил его, у меня все заработало. Я не уверен, что достаточно хорошо знаком с вашим источником, чтобы знать, может ли эта строка служить какой-либо цели, но в моем случае она казалась избыточной для перечисления в GetChunk.

downwitch 27.07.2016 20:42

Потратив некоторое время на это (и игнорируя мои предыдущие / скрытые комментарии), я согласен с CaseyB в том, что это отличное, элегантное и (в конечном итоге) простое решение. Но я утверждаю, что прокомментированная строка discard elements... не служит измеримой цели (оставшееся всегда будет <0 из-за do / while во «внутреннем» цикле), и это сбивает с толку проблему - попробуйте заменить int chunkSize на uint, и наслаждайся вечеринкой. (В противном случае, если вы «частично потребляете», вы все равно останавливаетесь перед этой чертой.) На всякий случай, если люди потеряют день или два, я пытался понять это.

downwitch 05.08.2016 00:56

Чтобы увидеть назначение while (innerMoveNext()), закомментируйте его и запустите c3.Count() из примеров.

3dGrabber 15.08.2016 13:08

К сожалению, последний пример использования у меня не работает: var take2 = c3.Select(c => c.Take(2)); Возвращает [[6], [6]]. Если я изменю его, добавив ToList () внутри Select (), то получу ожидаемый результат. Тем не менее, это очень полезно!

Gyromite 16.08.2019 23:02

Это старый вопрос, но именно этим я и закончил; он перечисляет перечислимые только один раз, но создает списки для каждого из разделов. Он не страдает от неожиданного поведения при вызове ToArray(), как это делают некоторые реализации:

    public static IEnumerable<IEnumerable<T>> Partition<T>(IEnumerable<T> source, int chunkSize)
    {
        if (source == null)
        {
            throw new ArgumentNullException("source");
        }

        if (chunkSize < 1)
        {
            throw new ArgumentException("Invalid chunkSize: " + chunkSize);
        }

        using (IEnumerator<T> sourceEnumerator = source.GetEnumerator())
        {
            IList<T> currentChunk = new List<T>();
            while (sourceEnumerator.MoveNext())
            {
                currentChunk.Add(sourceEnumerator.Current);
                if (currentChunk.Count == chunkSize)
                {
                    yield return currentChunk;
                    currentChunk = new List<T>();
                }
            }

            if (currentChunk.Any())
            {
                yield return currentChunk;
            }
        }
    }

Было бы хорошо преобразовать это в метод расширения: public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int chunkSize)

krizzzn 24.06.2014 18:30

+1 за ваш ответ. Однако я рекомендую две вещи: 1. использовать foreach вместо while и using block. 2. Передайте chunkSize в конструкторе List, чтобы список знал его максимальный ожидаемый размер.

Usman Zafar 21.07.2014 11:26

Я думаю, что следующее предложение будет самым быстрым. Я жертвую ленивостью источника Enumerable ради возможности использовать Array.Copy и заранее зная длину каждого из моих подсписок.

public static IEnumerable<T[]> Chunk<T>(this IEnumerable<T> items, int size)
{
    T[] array = items as T[] ?? items.ToArray();
    for (int i = 0; i < array.Length; i+=size)
    {
        T[] chunk = new T[Math.Min(size, array.Length - i)];
        Array.Copy(array, i, chunk, 0, chunk.Length);
        yield return chunk;
    }
}

Он не только самый быстрый, но и правильно обрабатывает дальнейшие перечисляемые операции с результатом, например items.Chunk (5) .Reverse (). SelectMany (x => x)

too 17.12.2019 00:40

Это следующее решение является самым компактным, что я мог придумать, это O (n).

public static IEnumerable<T[]> Chunk<T>(IEnumerable<T> source, int chunksize)
{
    var list = source as IList<T> ?? source.ToList();
    for (int start = 0; start < list.Count; start += chunksize)
    {
        T[] chunk = new T[Math.Min(chunksize, list.Count - start)];
        for (int i = 0; i < chunk.Length; i++)
            chunk[i] = list[start + i];

        yield return chunk;
    }
}

Чтобы вставить мои два цента ...

Используя тип списка для фрагментации источника, я нашел еще одно очень компактное решение:

public static IEnumerable<IEnumerable<TSource>> Chunk<TSource>(this IEnumerable<TSource> source, int chunkSize)
{
    // copy the source into a list
    var chunkList = source.ToList();

    // return chunks of 'chunkSize' items
    while (chunkList.Count > chunkSize)
    {
        yield return chunkList.GetRange(0, chunkSize);
        chunkList.RemoveRange(0, chunkSize);
    }

    // return the rest
    yield return chunkList;
}

полностью ленив, без подсчета и копирования:

public static class EnumerableExtensions
{

  public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> source, int len)
  {
     if (len == 0)
        throw new ArgumentNullException();

     var enumer = source.GetEnumerator();
     while (enumer.MoveNext())
     {
        yield return Take(enumer.Current, enumer, len);
     }
  }

  private static IEnumerable<T> Take<T>(T head, IEnumerator<T> tail, int len)
  {
     while (true)
     {
        yield return head;
        if (--len == 0)
           break;
        if (tail.MoveNext())
           head = tail.Current;
        else
           break;
     }
  }
}

Это решение настолько элегантно, что мне очень жаль, что я не могу проголосовать за этот ответ более одного раза.

Mark 10.04.2015 17:21

Я не думаю, что это когда-нибудь точно выйдет из строя. Но у него определенно могло быть странное поведение. Если бы у вас было 100 элементов, и вы разделили их на партии по 10, и вы перечислили все партии, не перечисляя какие-либо элементы этих партий, вы получите 100 партий из 1.

CaseyB 15.04.2015 23:58

Как упоминал @CaseyB, это страдает от того же отказа 3dGrabber, который упоминается здесь stackoverflow.com/a/20953521/1037948, но, черт возьми, это быстро!

drzaus 10.06.2015 21:04

Это красивое решение. Делает именно то, что обещает.

Rod Hartzell 17.02.2016 01:59

Безусловно, самое элегантное и точное решение. Единственное, вы должны добавить проверку на отрицательные числа и заменить ArgumentNullException на ArgumentException.

Romain Vergnory 02.04.2019 17:45

Старый код, но вот что я использовал:

    public static IEnumerable<List<T>> InSetsOf<T>(this IEnumerable<T> source, int max)
    {
        var toReturn = new List<T>(max);
        foreach (var item in source)
        {
            toReturn.Add(item);
            if (toReturn.Count == max)
            {
                yield return toReturn;
                toReturn = new List<T>(max);
            }
        }
        if (toReturn.Any())
        {
            yield return toReturn;
        }
    }

После публикации я понял, что это практически тот же код, который casperOne опубликовал 6 лет назад, с изменением использования .Any () вместо .Count (), поскольку мне не нужен весь счет, просто нужно знать, существуют ли какие-либо .

Robert McKee 25.02.2015 12:47

Я считаю, что этот небольшой фрагмент отлично справляется со своей задачей.

public static IEnumerable<List<T>> Chunked<T>(this List<T> source, int chunkSize)
{
    var offset = 0;

    while (offset < source.Count)
    {
        yield return source.GetRange(offset, Math.Min(source.Count - offset, chunkSize));
        offset += chunkSize;
    }
}

Это старое решение, но у меня был другой подход. Я использую Skip для перехода к желаемому смещению и Take для извлечения желаемого количества элементов:

public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, 
                                                   int chunkSize)
{
    if (chunkSize <= 0)
        throw new ArgumentOutOfRangeException($"{nameof(chunkSize)} should be > 0");

    var nbChunks = (int)Math.Ceiling((double)source.Count()/chunkSize);

    return Enumerable.Range(0, nbChunks)
                     .Select(chunkNb => source.Skip(chunkNb*chunkSize)
                     .Take(chunkSize));
}

Очень похоже на подход, который я использовал, но я рекомендую, чтобы источник не был IEnumerable. Например, если источник является результатом запроса LINQ, Skip / Take вызовет перечисление nbChunk запроса. Может быть дорого. Лучше было бы использовать IList или ICollection в качестве типа для источника. Это полностью позволяет избежать проблемы.

RB Davidson 27.04.2018 15:25

Просто кладу свои два цента. Если вы хотите «сгруппировать» список (визуализировать слева направо), вы можете сделать следующее:

 public static List<List<T>> Buckets<T>(this List<T> source, int numberOfBuckets)
    {
        List<List<T>> result = new List<List<T>>();
        for (int i = 0; i < numberOfBuckets; i++)
        {
            result.Add(new List<T>());
        }

        int count = 0;
        while (count < source.Count())
        {
            var mod = count % numberOfBuckets;
            result[mod].Add(source[count]);
            count++;
        }
        return result;
    }

Я взял первичный ответ и сделал его контейнером IOC, чтобы определить, где разделить. (Для тех, кто действительно хочет разделить только на 3 пункта, читая этот пост в поисках ответа?)

Этот метод позволяет при необходимости разбить на элементы любого типа.

public static List<List<T>> SplitOn<T>(List<T> main, Func<T, bool> splitOn)
{
    int groupIndex = 0;

    return main.Select( item => new 
                             { 
                               Group = (splitOn.Invoke(item) ? ++groupIndex : groupIndex), 
                               Value = item 
                             })
                .GroupBy( it2 => it2.Group)
                .Select(x => x.Select(v => v.Value).ToList())
                .ToList();
}

Итак, для OP код будет

var it = new List<string>()
                       { "a", "g", "e", "w", "p", "s", "q", "f", "x", "y", "i", "m", "c" };

int index = 0; 
var result = SplitOn(it, (itm) => (index++ % 3) == 0 );

Такой перформативный, как подход Сэм Шафран.

public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size), "Size must be greater than zero.");

    return BatchImpl(source, size).TakeWhile(x => x.Any());
}

static IEnumerable<IEnumerable<T>> BatchImpl<T>(this IEnumerable<T> source, int size)
{
    var values = new List<T>();
    var group = 1;
    var disposed = false;
    var e = source.GetEnumerator();

    try
    {
        while (!disposed)
        {
            yield return GetBatch(e, values, group, size, () => { e.Dispose(); disposed = true; });
            group++;
        }
    }
    finally
    {
        if (!disposed)
            e.Dispose();
    }
}

static IEnumerable<T> GetBatch<T>(IEnumerator<T> e, List<T> values, int group, int size, Action dispose)
{
    var min = (group - 1) * size + 1;
    var max = group * size;
    var hasValue = false;

    while (values.Count < min && e.MoveNext())
    {
        values.Add(e.Current);
    }

    for (var i = min; i <= max; i++)
    {
        if (i <= values.Count)
        {
            hasValue = true;
        }
        else if (hasValue = e.MoveNext())
        {
            values.Add(e.Current);
        }
        else
        {
            dispose();
        }

        if (hasValue)
            yield return values[i - 1];
        else
            yield break;
    }
}

}

Может работать с бесконечными генераторами:

a.Zip(a.Skip(1), (x, y) => Enumerable.Repeat(x, 1).Concat(Enumerable.Repeat(y, 1)))
 .Zip(a.Skip(2), (xy, z) => xy.Concat(Enumerable.Repeat(z, 1)))
 .Where((x, i) => i % 3 == 0)

Демо-код: https://ideone.com/GKmL7M

using System;
using System.Collections.Generic;
using System.Linq;

public class Test
{
  private static void DoIt(IEnumerable<int> a)
  {
    Console.WriteLine(String.Join(" ", a));

    foreach (var x in a.Zip(a.Skip(1), (x, y) => Enumerable.Repeat(x, 1).Concat(Enumerable.Repeat(y, 1))).Zip(a.Skip(2), (xy, z) => xy.Concat(Enumerable.Repeat(z, 1))).Where((x, i) => i % 3 == 0))
      Console.WriteLine(String.Join(" ", x));

    Console.WriteLine();
  }

  public static void Main()
  {
    DoIt(new int[] {1});
    DoIt(new int[] {1, 2});
    DoIt(new int[] {1, 2, 3});
    DoIt(new int[] {1, 2, 3, 4});
    DoIt(new int[] {1, 2, 3, 4, 5});
    DoIt(new int[] {1, 2, 3, 4, 5, 6});
  }
}
1

1 2

1 2 3
1 2 3

1 2 3 4
1 2 3

1 2 3 4 5
1 2 3

1 2 3 4 5 6
1 2 3
4 5 6

Но на самом деле я бы предпочел написать соответствующий метод без linq.

Для всех, кто интересуется пакетным / поддерживаемым решением, библиотека ПодробнееLINQ предоставляет метод расширения Batch, который соответствует вашему запрошенному поведению:

IEnumerable<char> source = "Example string";
IEnumerable<IEnumerable<char>> chunksOfThreeChars = source.Batch(3);

Реализация Batch аналогичен Ответ Кэмерона МакФарланда с добавлением перегрузки для преобразования фрагмента / пакета перед возвратом и работает довольно хорошо.

это должен быть принятый ответ. Вместо того, чтобы изобретать колесо, следует использовать morelinq

Otabek Kholikov 16.09.2019 15:41

Действительно. Проверял исходный код на github, он превосходит все на этой странице. Включая свой ответ :) Сначала я проверил moreLinq, но искал что-то с "Chunk" в названии.

Zar Shardan 21.06.2020 12:28

Другой способ - использовать 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();

ИМХО самый порпер ответ.

Stanislav Berkov 21.04.2019 20:11

Это тоже довольно лаконично. Похоже, что это должно быть в общей библиотеке linq со всеми взором и множеством тестов.

Craig.C 11.09.2020 11:35

Проверь это! У меня есть список элементов со счетчиком последовательности и датой. Каждый раз, когда последовательность перезапускается, я хочу создать новый список.

Бывший. список сообщений.

 List<dynamic> messages = new List<dynamic>
        {
            new { FcntUp = 101, CommTimestamp = "2019-01-01 00:00:01" },
            new { FcntUp = 102, CommTimestamp = "2019-01-01 00:00:02" },
            new { FcntUp = 103, CommTimestamp = "2019-01-01 00:00:03" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:04" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:05" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:06" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:07" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:08" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:09" }
        };

Я хочу разбить список на отдельные списки при перезапуске счетчика. Вот код:

var arraylist = new List<List<dynamic>>();

        List<dynamic> messages = new List<dynamic>
        {
            new { FcntUp = 101, CommTimestamp = "2019-01-01 00:00:01" },
            new { FcntUp = 102, CommTimestamp = "2019-01-01 00:00:02" },
            new { FcntUp = 103, CommTimestamp = "2019-01-01 00:00:03" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:04" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:05" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:06" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:07" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:08" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:09" }
        };

        //group by FcntUp and CommTimestamp
        var query = messages.GroupBy(x => new { x.FcntUp, x.CommTimestamp });

        //declare the current item
        dynamic currentItem = null;

        //declare the list of ranges
        List<dynamic> range = null;

        //loop through the sorted list
        foreach (var item in query)
        {
            //check if start of new range
            if (currentItem == null || item.Key.FcntUp < currentItem.Key.FcntUp)
            {
                //create a new list if the FcntUp starts on a new range
                range = new List<dynamic>();

                //add the list to the parent list
                arraylist.Add(range);
            }

            //add the item to the sublist
            range.Add(item);

            //set the current item
            currentItem = item;
        }
public static List<List<T>> GetSplitItemsList<T>(List<T> originalItemsList, short number)
    {
        var listGroup = new List<List<T>>();
        int j = number;
        for (int i = 0; i < originalItemsList.Count; i += number)
        {
            var cList = originalItemsList.Take(j).Skip(i).ToList();
            j += number;
            listGroup.Add(cList);
        }
        return listGroup;
    }

Если исходная коллекция реализует IList <T> (произвольный доступ по индексу), мы могли бы использовать следующий подход. Он извлекает элементы только тогда, когда к ним действительно обращаются, поэтому это особенно полезно для коллекций с отложенной оценкой. Подобно неограниченному IEnumerable <T>, но для IList <T>.

    public static IEnumerable<IEnumerable<T>> Chunkify<T>(this IList<T> src, int chunkSize)
    {
        if (src == null) throw new ArgumentNullException(nameof(src));
        if (chunkSize < 1) throw new ArgumentOutOfRangeException(nameof(chunkSize), $"must be > 0, got {chunkSize}");

        for(var ci = 0; ci <= src.Count/chunkSize; ci++){
            yield return Window(src, ci*chunkSize, Math.Min((ci+1)*chunkSize, src.Count)-1);
        }
    }

    private static IEnumerable<T> Window<T>(IList<T> src, int startIdx, int endIdx)
    {
        Console.WriteLine($"window {startIdx} - {endIdx}");
        while(startIdx <= endIdx){
            yield return src[startIdx++];
        }
    }

Невозможно объединить все желаемые функции, такие как полная ленивость, отсутствие копирования, полная универсальность и безопасность в одном решении. Самая фундаментальная причина заключается в том, что нельзя гарантировать, что входные данные не изменятся до того, как будут доступны фрагменты. Предположим, что у нас есть функция следующей сигнатуры:

public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunkSize)
{
    // Some implementation
}

Тогда следующий способ его использования проблематичен:

var myList = new List<int>()
{
    1,2,3,4
};
var myChunks = myList.Chunk(2);
myList.RemoveAt(0);
var firstChunk = myChunks.First();    
Console.WriteLine("First chunk:" + String.Join(',', firstChunk));
myList.RemoveAt(0);
var secondChunk = myChunks.Skip(1).First();
Console.WriteLine("Second chunk:" + String.Join(',', secondChunk));
// What outputs do we see for first and second chunk? Probably not what you would expect...

В зависимости от конкретной реализации код либо завершится ошибкой во время выполнения, либо выдаст неинтуитивные результаты.

Значит, нужно ослабить хотя бы одно из свойств. Если вам нужно пуленепробиваемое ленивое решение, вам нужно ограничить свой тип ввода неизменяемым типом, и даже в этом случае непросто скрыть все варианты использования. Однако, если у вас есть контроль над использованием, вы все равно можете выбрать наиболее общее решение, если убедитесь, что оно используется надлежащим образом. В противном случае вы можете отказаться от лени и согласиться на некоторое копирование.

В конце концов, все зависит от вашего варианта использования и требований, какое решение будет для вас лучшим выбором.

Вопрос был в том, как «Разделить список на подсписки с LINQ», но иногда вы можете захотеть, чтобы эти подсписки были ссылками на исходный список, а не на копии. Это позволяет вам изменять исходный список из подсписок. В этом случае это может сработать для вас.

public static IEnumerable<Memory<T>> RefChunkBy<T>(this T[] array, int size)
{
    if (size < 1 || array is null)
    {
        throw new ArgumentException("chunkSize must be positive");
    }

    var index = 0;
    var counter = 0;

    for (int i = 0; i < array.Length; i++)
    {
        if (counter == size)
        {
            yield return new Memory<T>(array, index, size);
            index = i;
            counter = 0;
        }
        counter++;

        if (i + 1 == array.Length)
        {
            yield return new Memory<T>(array, index, array.Length - index);
        }
    }
}

Применение:

var src = new[] { 1, 2, 3, 4, 5, 6 };

var c3 = RefChunkBy(src, 3);      // {{1, 2, 3}, {4, 5, 6}};
var c4 = RefChunkBy(src, 4);      // {{1, 2, 3, 4}, {5, 6}};

// as extension method
var c3 = src.RefChunkBy(3);      // {{1, 2, 3}, {4, 5, 6}};
var c4 = src.RefChunkBy(4);      // {{1, 2, 3, 4}, {5, 6}};

var sum = c3.Select(c => c.Span.ToArray().Sum());    // {6, 15}
var count = c3.Count();                 // 2
var take2 = c3.Select(c => c.Span.ToArray().Take(2));  // {{1, 2}, {4, 5}}

Не стесняйтесь улучшать этот код.

Другие вопросы по теме