Список разделов LINQ в списки из 8 членов
Как взять список (используя LINQ) и разбить его на список списков, разделяющих исходный список на каждую восьмую запись?
Я предполагаю, что что-то вроде этого будет включать Skip и/или Take, но я все еще довольно новичок в LINQ.
Изменить: использование С#/.Net 3.5
Edit2: Этот вопрос сформулирован иначе, чем другой "дублирующий" вопрос. Хотя проблемы схожи, ответы в этом вопросе превосходят: "принятый" ответ очень твердый (с выражением yield
), а также предложение Jon Skeet использовать MoreLinq (что не рекомендуется в "других" вопрос). Иногда дубликаты хороши тем, что они заставляют пересмотреть проблему.
Ответы
Ответ 1
Используйте следующий метод расширения, чтобы разбить вход на подмножества
public static class IEnumerableExtensions
{
public static IEnumerable<List<T>> InSetsOf<T>(this IEnumerable<T> source, int max)
{
List<T> 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;
}
}
}
Ответ 2
У нас есть только такой метод в MoreLINQ как Batch:
// As IEnumerable<IEnumerable<T>>
var items = list.Batch(8);
или
// As IEnumerable<List<T>>
var items = list.Batch(8, seq => seq.ToList());
Ответ 3
Вам лучше использовать библиотеку, например MoreLinq, но если вам действительно нужно было это сделать, используя "plain LINQ", вы можете использовать GroupBy
:
var sequence = new[] {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};
var result = sequence.Select((x, i) => new {Group = i/8, Value = x})
.GroupBy(item => item.Group, g => g.Value)
.Select(g => g.Where(x => true));
// result is: { {1,2,3,4,5,6,7,8}, {9,10,11,12,13,14,15,16} }
В принципе, мы используем версию Select()
, которая предоставляет индекс для потребляемого значения, мы делим индекс на 8, чтобы определить, к какой группе принадлежит каждое значение. Затем мы группируем последовательность с помощью этого ключа группировки. Последний Select
просто уменьшает IGrouping<>
до IEnumerable<IEnumerable<T>>
(и не является строго необходимым, так как IGrouping
является IEnumerable
).
Достаточно легко превратить это в метод многократного использования путем факторизации нашей константы 8
в примере и замены ее на указанный параметр.
Это не обязательно самое элегантное решение, и это уже не ленивое, потоковое решение... но оно действительно работает.
Вы также можете написать свой собственный метод расширения с использованием блоков итератора (yield return
), который может дать вам лучшую производительность и использовать меньше памяти, чем GroupBy
. Это то, что метод Batch()
MoreLinq делает IIRC.
Ответ 4
from b in Enumerable.Range(0,8) select items.Where((x,i) => (i % 8) == b);
Ответ 5
Take не будет очень эффективным, потому что он не удаляет сделанные записи.
почему бы не использовать простой цикл:
public IEnumerable<IList<T>> Partition<T>(this/* <-- see extension methods*/ IEnumerable<T> src,int num)
{
IEnumerator<T> enu=src.getEnumerator();
while(true)
{
List<T> result=new List<T>(num);
for(int i=0;i<num;i++)
{
if(!enu.MoveNext())
{
if(i>0)yield return result;
yield break;
}
result.Add(enu.Current);
}
yield return result;
}
}
Ответ 6
Это совсем не то, что имели в виду первоначальные дизайнеры Linq, но проверьте это злоупотребление GroupBy:
public static IEnumerable<IEnumerable<T>> BatchBy<T>(this IEnumerable<T> items, int batchSize)
{
var count = 0;
return items.GroupBy(x => (count++ / batchSize)).ToList();
}
[TestMethod]
public void BatchBy_breaks_a_list_into_chunks()
{
var values = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var batches = values.BatchBy(3);
batches.Count().ShouldEqual(4);
batches.First().Count().ShouldEqual(3);
batches.Last().Count().ShouldEqual(1);
}
Я думаю, что он выигрывает премию "гольф" за этот вопрос. ToList
очень важен, так как вы хотите убедиться, что группировка действительно была выполнена, прежде чем пытаться что-либо сделать с выходом. Если вы удалите ToList
, вы получите некоторые странные побочные эффекты.
Ответ 7
Простейшее решение задается Мелом:
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> items,
int partitionSize)
{
int i = 0;
return items.GroupBy(x => i++ / partitionSize).ToArray();
}
Краткий, но медленный. Вышеупомянутый метод разбивает IEnumerable на куски требуемого фиксированного размера, при этом общее количество кусков не имеет значения. Чтобы разделить IEnumerable на N количество кусков равных размеров или близких к равным размерам, вы можете сделать:
public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> items,
int numOfParts)
{
int i = 0;
return items.GroupBy(x => i++ % numOfParts);
}
Чтобы ускорить работу, можно было бы сделать простой подход:
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> items,
int partitionSize)
{
if (partitionSize <= 0)
throw new ArgumentOutOfRangeException("partitionSize");
int innerListCounter = 0;
int numberOfPackets = 0;
foreach (var item in items)
{
innerListCounter++;
if (innerListCounter == partitionSize)
{
yield return items.Skip(numberOfPackets * partitionSize).Take(partitionSize);
innerListCounter = 0;
numberOfPackets++;
}
}
if (innerListCounter > 0)
yield return items.Skip(numberOfPackets * partitionSize);
}
Это сейчас быстрее, чем что-либо в настоящее время на планете:). Эквивалентные методы для операции Split
здесь