Есть ли способ разделить 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]
Мне также нужно, чтобы размер результирующих списков был параметром этой функции.





Если список имеет тип 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,
Важное замечание: нет будет работать, если вы не слейте всю дочернюю последовательность или не прервете какой-либо момент в родительской последовательности. Это важное предостережение, но если ваш вариант использования заключается в том, что вы будете использовать элемент каждый последовательности последовательностей, тогда это сработает для вас.
Кроме того, он будет делать странные вещи, если вы играете с порядком, точно так же, как Сэм сделал в какой-то момент.
@ Амир, я бы хотел, чтобы это было написано
Это красиво и быстро - Кэмерон опубликовал очень похожий, после вашего, с той лишь оговоркой, что он буферизует куски, это может привести к нехватке памяти, если куски и размеры элементов большие. Обратитесь к моему ответу за альтернативным, хотя и более сложным ответом.
@SamSaffron Да, если у вас есть большое количество элементов в List<T>, у вас, очевидно, будут проблемы с памятью из-за буферизации. Оглядываясь назад, я должен был отметить это в ответе, но в то время казалось, что основное внимание уделялось слишком большому количеству итераций. Тем не менее, ваше решение действительно более сложное. Я не тестировал его, но теперь мне интересно, есть ли менее сложное решение.
@casperOne, да ... Google дал мне эту страницу, когда я искал способ разделить перечислимые объекты, для моего конкретного случая использования я разбиваю безумно большой список записей, которые возвращаются из базы данных, если я материализую их в список, который он взорвет (на самом деле у dapper есть опция buffer: false только для этого варианта использования)
любопытно увидеть более чистое решение, которое поддерживает ту же производительность, это очень интересная проблема
@ SamSaffron Я займусь этим сегодня. Вам нужна потокобезопасность? Я столкнулся с этой проблемой при переносе вещей из базы данных очень давно. Разбиение на части в базе данных (если ваши таблицы позволяют это) очень помогает (у меня есть сверхглубокая таблица с сотнями миллионов строк, на которых у меня есть естественные разделы, поэтому я могу разбивать на части на уровне базы данных, а запросы не сходят с ума).
Безопасность потоков на самом деле не является проблемой, которую мне нужно решать, мое решение было бы легко адаптировать к потокобезопасному довольно легко, но многопоточность, вероятно, немного ухудшит производительность, если не будут внесены какие-либо другие настройки, эта проблема показалась мне довольно интересной, что является почему я потратил на это час или два.
изначально я передавал счетчики, и моя реализация была намного проще, но Enumerable.Range(0, 100).Chunk(3).Reverse().ToArray() было действительно сложно исправить
Изначально мне нужна была безопасность потоков, так как мой запрос linq был context.Select (GetJobData) .Clump (10) .AsParallel (). Select (Pro cessJobs), и мне нужно было сгруппировать задания, чтобы поток работал в течение разумного периода времени. вместо массового переключения потоков. :П
@Sam Saffron: «Enumerable.Range (0, 100) .Chunk (3) .Reverse (). ToArray ()» можно исправить простым: «Enumerable.Range (0, 100) .Chunk (3) .Select (e => e.ToList ()). Reverse (). ToArray () "до тех пор, пока вы работаете с небольшими наборами данных ... Поскольку он, очевидно, материализует все данные в памяти заранее.
@Sam Saffron: Если вам нужно что-то для больших наборов данных из базы данных, я бы подумал, что нужно глубоко изучить деревья выражений (например, LINQ to SQL, если это база данных такого типа) и каким-то образом найти решение, которое позволило бы каждому «фрагменту» или «Раздел» должен быть отображен под ним ... То есть офс. не простые вещи ^^
Это явный победитель с точки зрения компромисса между эффективностью и простотой. Слишком много комментариев :)
Попробуйте следующий код.
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.
@Justice, было бы неплохо иметь встроенную систему разделения для IEnumerable.
@JaredPar Вы можете сделать это достаточно легко с помощью метода расширения. Я подозреваю, что его там нет, отчасти потому, что он плохо интегрируется с SQL. 'myEnumerable.InGroupsOf (3) .Select (subEnumerable => subEnumerable.Sum ()). Average ()' плюс перегрузки было бы неплохо.
@Justice, GroupBy может быть реализовано с помощью хеширования. Откуда вы знаете, что реализация GroupBy «может убить производительность»?
GroupBy ничего не возвращает, пока не перечислит все элементы. Вот почему это медленно. Списки, которые хочет OP, являются смежными, поэтому лучший метод мог бы выдать первый подсписок [a,g,e] перед перечислением каких-либо дополнительных элементов исходного списка.
Возьмем крайний пример бесконечного IEnumerable. GroupBy(x=>f(x)).First() никогда не уступит группу. OP спросил о списках, но если мы напишем для работы с IEnumerable, сделав только одну итерацию, мы получим преимущество в производительности.
Стоит отметить, что .GroupBy(x => x.Index % 3) разделит всю коллекцию поровну на 3 части, поэтому, если у вас есть 30 элементов, вы получите 3 списка из 10. Текущий пример дает вам 10 списков из 3, если у вас их 30.
@Nick Order не сохранился для вас. Это все еще полезно знать, но вы бы сгруппировали их в (0,3,6,9, ...), (1,4,7,10, ...), (2,5,8 , 11, ...). Если порядок не имеет значения, тогда все в порядке, но в данном случае кажется, что это имеет значение.
Проверьте использование MoreLinq в этом посте: stackoverflow.com/questions/13731796/create-batches-in-linq
Разве этот простой код int i=0; return source.GroupBy(x => (i++/3)).ToList() не работал бы? Он отлично работает для меня.
Не удается преобразовать общий список <list <t>> в общий ilist <ilist <t>>?
заменить IList<IList<T>> Split<T>(IList<T> source) на IList<List<T>> Split<T>(IList<T> source)
У меня был хороший успех при использовании .GroupBy(x => Math.Round(x.Index / chunkSize))
на последнем .ToList();: Ошибка 45 Невозможно неявно преобразовать тип System.Collections.Generic.List<System.Collections.Generic.List<T>> в System.Collections.Generic.IList<System.Collections.Generic.IList<T>>. Существует явное преобразование (вам не хватает приведения?)
@yfeldblum, пожалуйста, посмотрите мой ответ без группы stackoverflow.com/a/53532961/3360759
Вот процедура разделения списка, которую я написал пару месяцев назад:
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();
Это хорошо, но сильно отличается от того, о чем просил исходный вопрос.
Я только что написал это и думаю, что это немного элегантнее, чем другие предлагаемые решения:
/// <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");
Мне это нравится, но это не очень эффективно
Я реализовал свой метод фрагментов IQueryable <> таким образом, за исключением того, что я сделал Count (), чтобы избежать n-1 .Any (). Я предполагаю, что оптимальная реализация зависит от контекста для IQueryables (в моем случае попадание в базу данных с LINQ to SQL).
Для последовательности из 13 элементов с размером блока 3 этот алгоритм будет обращаться к элементам 91 раз. Это не проблема для маленьких последовательностей, но очень неэффективно для больших.
Мне нравится этот, но эффективность по времени - O(n²). Вы можете просмотреть список и получить время O(n).
@hIpPy, как это n ^ 2? Мне кажется линейным
@vivekmaharajh source каждый раз заменяется упакованным IEnumerable. Таким образом, взятие элементов из source проходит через слои Skip.
@hIpPy Это действительно будет O (n ^ 2) для IEnumerable с задержкой. Однако, если у вас есть старый добрый array[] или List, это будет O(n). Если вам нравится это решение и у вас есть IEnumerable, вы можете использовать .ToArray(), прежде чем передавать его в Chunk, и получить O(n). Конечно, недостатками являются дополнительная память и потенциальная дополнительная производительность (если у вас есть вызывающие абоненты, которые могут не перечислять все фрагменты, вы излишне перечисляете все source). Из-за этих недостатков ручное выполнение итерации самостоятельно, как предлагает @hlpPy, вероятно, является лучшим вариантом для производственного кода.
@vivekmaharajh: как вы думаете, почему этот список или массив делает его 0(n), они не оптимизированы, а Skip и Take всегда будут перечислять последовательность до этого момента, поэтому имеет сложность O(n^2). В очень большом списке аппорации Skip / Take бесполезны. Я бы использовал GroupBy с целочисленным делением или (более эффективно) Batch от LINQ.
Вы не можете получить O (n) по простой причине: вы можете перебирать внешний и внутренний счетчики в случайном порядке. Единственное улучшение этого подхода - объединение Any () и Take ().
Супер элегантный ответ.
В целом подход, предложенный 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 () генерирует исключение?
@SamSaffron Я обновил свой ответ и значительно упростил код для того, что я считаю наиболее распространенным вариантом использования (и признаю предостережения).
А как насчет разбиения на части IQueryable <>? Я предполагаю, что подход Take / Skip будет оптимальным, если мы хотим делегировать максимум операций поставщику.
@ Guillaume86 Я согласен, если у вас есть IList или IQueryable, вы можете использовать всевозможные ярлыки, которые сделают это намного быстрее (Linq делает это внутренне для всех видов других методов)
Это, безусловно, лучший ответ по эффективности. У меня проблема с использованием SqlBulkCopy с IEnumerable, который запускает дополнительные процессы в каждом столбце, поэтому он должен эффективно выполняться только за один проход. Это позволит мне разбить IEnumerable на куски управляемого размера. (Для тех, кому интересно, я включил потоковый режим SqlBulkCopy, который, похоже, не работает).
Это сократило простой метод linq с 6 секунд до нуля. Действительно быстро!
Создавая свою собственную версию, не глядя на вашу, я думаю, что Select мог бы быть намного более эффективным, если бы он пропустил вызов функции selector, если не вызывается Current (по общему признанию, вызывая игнорирование побочных эффектов в selector - возможно, selector можно было бы оценить на предмет ссылок без параметров) .
Для этой цели System.Interactive предоставляет Buffer(). Некоторые быстрые тесты показывают, что производительность аналогична решению Сэма.
вы знаете семантику буферизации? например: если у вас есть счетчик, который выдает строки размером 300 КБ и пытается разбить их на блоки размером 10 000, вы получите нехватку памяти?
Buffer() возвращает IEnumerable<IList<T>>, так что да, у вас, вероятно, возникнут проблемы - он не транслируется, как ваш.
Несколько лет назад я написал метод расширения 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% кусков, я пытался этого избежать ... но это оказалось невероятно опасным.
@SamSaffron Ага. Особенно, если вы добавите в микс такие вещи, как plinq, для чего изначально была моя реализация.
расширил свой ответ, дайте мне знать, что вы думаете
@CameronMacFarland - можете ли вы объяснить, почему необходима вторая проверка count == size? Спасибо.
Мы можем улучшить решение @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 () линейно по количеству взятых предметов. Так что это должно работать хорошо.
Хорошо, вот мое мнение:
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 ИМХО.
Это страдает неожиданным (с точки зрения API) поведением при вызове ToArray (), а также не является потокобезопасным.
@aolszowka: не могли бы вы уточнить?
@ 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 не увеличивался.
@ 3dGrabber (продолжение) Кроме того, поскольку внутренние перечисления не генерируются до тех пор, пока к ним не обращаются, попытка выполнить какой-либо тип поточной операции над ними может привести к состоянию гонки, в котором поток вызывает sourceEnumerator.MoveNext () до того, как другой поток читает sourceEnumerator. Текущий. По крайней мере, это то, что я придумал, если я ошибаюсь, дайте мне знать, я хочу учиться!
@aolszowka: очень важные моменты. Я добавил предупреждение и раздел использования. Код предполагает, что вы перебираете внутреннее перечислимое. Однако с вашим решением вы избавляетесь от лени. Я думаю, что можно получить лучшее из обоих миров с помощью настраиваемого кеширующего IEnumerator. Если найду решение, выложу здесь ...
yield return enumerator.GetChunk (chunkSize) .ToArray (); это простое решение за счет буферизации. Другой вариант - выбросить исключение, когда предыдущее внутреннее перечислимое не исчерпано, когда в следующий раз будет возвращено внешнее. (Я подозреваю, что вы думаете о буферизации только в случае необходимости; это тоже было бы круто.)
@ 3dGrabber Я пытаюсь использовать это (потому что элегантно) для неленивого случая, чтобы разделить большие коллекции сложных объектов (в основном, get и .ToList ()), но не могу заставить его вернуть больше, чем первый кусок . Нет специального перечислителя. Понимая, что это расплывчато, есть идеи, почему это может произойти с прямой (не универсальной) копией этого?
@ 3dGrabber ничего, извините за беспокойство. В моем случае я расширил innerMoveNext для ведения журнала и смог увидеть, что проблема была вызвана вызовом while (innerMoveNext()) {/* discard elements skipped by inner iterator */} - как только я удалил его, у меня все заработало. Я не уверен, что достаточно хорошо знаком с вашим источником, чтобы знать, может ли эта строка служить какой-либо цели, но в моем случае она казалась избыточной для перечисления в GetChunk.
Потратив некоторое время на это (и игнорируя мои предыдущие / скрытые комментарии), я согласен с CaseyB в том, что это отличное, элегантное и (в конечном итоге) простое решение. Но я утверждаю, что прокомментированная строка discard elements... не служит измеримой цели (оставшееся всегда будет <0 из-за do / while во «внутреннем» цикле), и это сбивает с толку проблему - попробуйте заменить int chunkSize на uint, и наслаждайся вечеринкой. (В противном случае, если вы «частично потребляете», вы все равно останавливаетесь перед этой чертой.) На всякий случай, если люди потеряют день или два, я пытался понять это.
Чтобы увидеть назначение while (innerMoveNext()), закомментируйте его и запустите c3.Count() из примеров.
К сожалению, последний пример использования у меня не работает: var take2 = c3.Select(c => c.Take(2)); Возвращает [[6], [6]]. Если я изменю его, добавив ToList () внутри Select (), то получу ожидаемый результат. Тем не менее, это очень полезно!
Это старый вопрос, но именно этим я и закончил; он перечисляет перечислимые только один раз, но создает списки для каждого из разделов. Он не страдает от неожиданного поведения при вызове 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)
+1 за ваш ответ. Однако я рекомендую две вещи: 1. использовать foreach вместо while и using block. 2. Передайте chunkSize в конструкторе List, чтобы список знал его максимальный ожидаемый размер.
Я думаю, что следующее предложение будет самым быстрым. Я жертвую ленивостью источника 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)
Это следующее решение является самым компактным, что я мог придумать, это 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;
}
}
}
Это решение настолько элегантно, что мне очень жаль, что я не могу проголосовать за этот ответ более одного раза.
Я не думаю, что это когда-нибудь точно выйдет из строя. Но у него определенно могло быть странное поведение. Если бы у вас было 100 элементов, и вы разделили их на партии по 10, и вы перечислили все партии, не перечисляя какие-либо элементы этих партий, вы получите 100 партий из 1.
Как упоминал @CaseyB, это страдает от того же отказа 3dGrabber, который упоминается здесь stackoverflow.com/a/20953521/1037948, но, черт возьми, это быстро!
Это красивое решение. Делает именно то, что обещает.
Безусловно, самое элегантное и точное решение. Единственное, вы должны добавить проверку на отрицательные числа и заменить ArgumentNullException на ArgumentException.
Старый код, но вот что я использовал:
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 (), поскольку мне не нужен весь счет, просто нужно знать, существуют ли какие-либо .
Я считаю, что этот небольшой фрагмент отлично справляется со своей задачей.
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 в качестве типа для источника. Это полностью позволяет избежать проблемы.
Просто кладу свои два цента. Если вы хотите «сгруппировать» список (визуализировать слева направо), вы можете сделать следующее:
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
Действительно. Проверял исходный код на github, он превосходит все на этой странице. Включая свой ответ :) Сначала я проверил moreLinq, но искал что-то с "Chunk" в названии.
Другой способ - использовать 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();
ИМХО самый порпер ответ.
Это тоже довольно лаконично. Похоже, что это должно быть в общей библиотеке linq со всеми взором и множеством тестов.
Проверь это! У меня есть список элементов со счетчиком последовательности и датой. Каждый раз, когда последовательность перезапускается, я хочу создать новый список.
Бывший. список сообщений.
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}}
Не стесняйтесь улучшать этот код.
Я думаю, что это лучшее решение ... единственная проблема в том, что у списка нет Length ... у него есть Count. Но это легко изменить. Мы можем сделать это лучше, даже не создавая списки, а возвращая ienumerables, которые содержат ссылки на основной список с комбинацией смещения / длины. Итак, если размер группы большой, мы не тратим впустую память. Прокомментируйте, если хотите, чтобы я это написал.