Разделите IEnumerable <T> на куски фиксированного размера (верните IEnumerable <IEnumerable <T>>, где внутренние последовательности имеют фиксированную длину)
Я хочу взять IEnumerable<T>
и разделить его на куски фиксированного размера.
У меня есть это, но кажется, что он неэлегантен из-за всего создания/копирования списка:
private static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> items, int partitionSize)
{
List<T> partition = new List<T>(partitionSize);
foreach (T item in items)
{
partition.Add(item);
if (partition.Count == partitionSize)
{
yield return partition;
partition = new List<T>(partitionSize);
}
}
// Cope with items.Count % partitionSize != 0
if (partition.Count > 0) yield return partition;
}
Есть ли что-то более идиоматическое?
EDIT: хотя это было отмечено как дубликат Разделить массив на массив массива подпоследовательности, это не так - этот вопрос имеет дело с расщеплением массива, тогда как это составляет около IEnumerable<T>
. Кроме того, этот вопрос требует, чтобы последняя подпоследовательность была дополнена. Эти два вопроса тесно связаны, но не совпадают.
Ответы
Ответ 1
Вы можете попытаться реализовать описанный выше пакетный метод следующим образом:
static class MyLinqExtensions
{
public static IEnumerable<IEnumerable<T>> Batch<T>(
this IEnumerable<T> source, int batchSize)
{
using (var enumerator = source.GetEnumerator())
while (enumerator.MoveNext())
yield return YieldBatchElements(enumerator, batchSize - 1);
}
private static IEnumerable<T> YieldBatchElements<T>(
IEnumerator<T> source, int batchSize)
{
yield return source.Current;
for (int i = 0; i < batchSize && source.MoveNext(); i++)
yield return source.Current;
}
}
Я захватил этот код из http://blogs.msdn.com/b/pfxteam/archive/2012/11/16/plinq-and-int32-maxvalue.aspx.
ОБНОВЛЕНИЕ. Обратите внимание, что эта реализация не только лениво оценивает партии, но также и элементы внутри партий, что означает, что они будут давать только правильные результаты, когда партия перечислится только после того, как все предыдущие партии были перечислены. Например:
public static void Main(string[] args)
{
var xs = Enumerable.Range(1, 20);
Print(xs.Batch(5).Skip(1)); // should skip first batch with 5 elements
}
public static void Print<T>(IEnumerable<IEnumerable<T>> batches)
{
foreach (var batch in batches)
{
Console.WriteLine($"[{string.Join(", ", batch)}]");
}
}
выведет:
[2, 3, 4, 5, 6] //only first element is skipped.
[7, 8, 9, 10, 11]
[12, 13, 14, 15, 16]
[17, 18, 19, 20]
Итак, если вы используете случай, который предполагает пакетную обработку, когда партии последовательно оцениваются, то ленивое решение выше будет работать, в противном случае, если вы не можете гарантировать строго последовательную пакетную обработку (например, когда вы хотите обрабатывать партии параллельно), вы, вероятно, будете нужно решение, которое с нетерпением перечисляет содержимое партии, похожее на упомянутое в вопросе выше или в MoreLINQ
Ответ 2
Похоже, вы хотите два блока итератора ( "yield return
methods" ). Я написал этот метод расширения:
static class Extensions
{
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> items, int partitionSize)
{
return new PartitionHelper<T>(items, partitionSize);
}
private sealed class PartitionHelper<T> : IEnumerable<IEnumerable<T>>
{
readonly IEnumerable<T> items;
readonly int partitionSize;
bool hasMoreItems;
internal PartitionHelper(IEnumerable<T> i, int ps)
{
items = i;
partitionSize = ps;
}
public IEnumerator<IEnumerable<T>> GetEnumerator()
{
using (var enumerator = items.GetEnumerator())
{
hasMoreItems = enumerator.MoveNext();
while (hasMoreItems)
yield return GetNextBatch(enumerator).ToList();
}
}
IEnumerable<T> GetNextBatch(IEnumerator<T> enumerator)
{
for (int i = 0; i < partitionSize; ++i)
{
yield return enumerator.Current;
hasMoreItems = enumerator.MoveNext();
if (!hasMoreItems)
yield break;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}
Ответ 3
Может быть,?
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> items, int partitionSize)
{
return items.Select((item, inx) => new { item, inx })
.GroupBy(x => x.inx / partitionSize)
.Select(g => g.Select(x => x.item));
}
Существует уже реализованная: morelinq Batch.
Ответ 4
Самое сумасшедшее решение (с Reactive Extensions):
public static IEnumerable<IList<T>> Partition<T>(this IEnumerable<T> items, int partitionSize)
{
return items
.ToObservable() // Converting sequence to observable sequence
.Buffer(partitionSize) // Splitting it on spececified "partitions"
.ToEnumerable(); // Converting it back to ordinary sequence
}
Я знаю, что я сменил подпись, но в любом случае мы все знаем, что у нас будет коллекция фиксированного размера как кусок.
BTW, если вы будете использовать блок итератора, не забудьте разделить вашу реализацию на два метода, чтобы с уверенностью утверждать аргументы!
Ответ 5
Для элегантного решения вы также можете посмотреть MoreLinq.Batch.
Он отправляет исходную последовательность в размерные ведра.
Пример:
int[] ints = new int[] {1,2,3,4,5,6};
var batches = ints.Batch(2); // batches -> [0] : 1,2 ; [1]:3,4 ; [2] :5,6
Ответ 6
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> items,
int partitionSize)
{
int i = 0;
return items.GroupBy(x => i++ / partitionSize).ToArray();
}
Ответ 7
Вы можете сделать это, используя перегрузку Enumerable.GroupBy
и используя преимущества целочисленного деления.
return items.Select((element, index) => new { Element = element, Index = index })
.GroupBy(obj => obj.Index / partitionSize, (_, partition) => partition);
Ответ 8
Как насчет классов разделителей в пространстве имен System.Collections.Concurrent?