Список разделов LINQ на списки из 8 членов

Как взять список (используя LINQ) и разбить его на список списков, разделяя исходный список на каждую 8-ю запись?

Я предполагаю, что что-то вроде этого будет включать Skip и / или Take, но я все еще новичок в LINQ.

Изменить: использование C # / .Net 3.5

Edit2: этот вопрос сформулирован иначе, чем другой «повторяющийся» вопрос. Хотя проблемы схожи, ответы на этот вопрос превосходны: как «принятый» ответ очень надежен (с выражением yield), так и предложение Джона Скита использовать MoreLinq (что не рекомендуется в «другом» вопросе. ) Иногда дубликаты хороши тем, что заставляют пересмотреть проблему.


person Pretzel    schedule 22.09.2010    source источник
comment
Вы используете VB или C #? Наличие итераторов имеет большое значение.   -  person Craig Gidney    schedule 23.09.2010
comment
Это не дубликат. Другой вопрос хотел разбить список на подсписки каждого n-го элемента, поэтому список с элементами 0, 8, 16, 24 и т. Д. И список с элементами 1, 9, 17, 25 и т. Д. И список с элементами 2, 10, 18 и т. д. Этот пользователь хочет разбить на список с 0..7, список с 8..15 и список с 16..24, аналогично разбиению на страницы   -  person Harald Coppoolse    schedule 01.02.2016


Ответы (7)


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

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;
        }
    }
}
person Handcraftsman    schedule 22.09.2010
comment
Я собираюсь попробовать это сейчас, так как это кажется довольно умным ... Мысль о доходности возврата пришла мне в голову, когда я обдумывал это, но я не видел четкого способа сделать это ... Я позволю вам знаю, как это работает для меня. - person Pretzel; 23.09.2010
comment
Вау! Это чертовски круто. Я пойду с этим! Спасибо за помощь! :-) - person Pretzel; 23.09.2010

У нас есть такой метод в MoreLINQ как Пакетный метод:

// As IEnumerable<IEnumerable<T>>
var items = list.Batch(8);

or

// As IEnumerable<List<T>>
var items = list.Batch(8, seq => seq.ToList());
person Jon Skeet    schedule 22.09.2010
comment
Круто, это реализовано очень хорошо, включая resultSelector (важно для управления / упорядочивания внутреннего списка). - person Kirk Woll; 23.09.2010
comment
Уф. Я подумал, что, возможно, я был немного глуп, не имея возможности понять это. Приятно видеть, что некоторые вещи отсутствуют в обычных LINQ-to-Objects. :) - person Pretzel; 23.09.2010
comment
@Pretzel: Дело не в том, что это невозможно с использованием простого старого LINQ ... просто это не очень эффективно и не просто для понимания. См. Мой ответ для простого примера LINQ. - person LBushkin; 23.09.2010
comment
+1, спасибо за ссылку на эту библиотеку. Я посмотрю, смогу ли я использовать его в будущих проектах. - person Pretzel; 23.09.2010
comment
Незначительный вопрос - я случайно скопировал код Batch, и Resharper предупреждает меня, что в return Batch(source, size, x => x); x возможна проблема с множественным перечислением. Правильно / игнорировать? - person drzaus; 11.06.2015

Вам лучше использовать такую ​​библиотеку, как MoreLinq, но если вам действительно нужно было сделать это с помощью " простой 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.

person LBushkin    schedule 22.09.2010
comment
Спасибо за ваш вклад. Да, это не кажется эффективным, и, как вы понимаете, я изо всех сил пытался понять, как я могу сделать это с помощью обычного LINQ. (Я смотрю на ваш ответ прямо сейчас, и я действительно не очень хорошо его понимаю.) Мне придется повозиться с ним позже. (Еще раз спасибо!) - person Pretzel; 23.09.2010
comment
Подход, использующий GroupBy(), не работает, если последовательность, которую вы планируете пакетировать, будет чрезвычайно большой (или бесконечной). Что касается того, как это работает - он создает анонимный объект, который связывает каждый элемент с его индексом, а затем группирует его в серию последовательностей на основе делимости на 8 (или любую другую ненулевую константу). - person LBushkin; 23.09.2010

Это совсем не то, что имели в виду оригинальные дизайнеры 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, вы получите странные побочные эффекты.

person Mel    schedule 13.10.2010
comment
Для справки, версия Handcraftsman, основанная на доходности и доходности, работает намного лучше, но мне все еще нравится Hey, вы не должны выполнять этот аспект этого кода. - person Mel; 14.10.2010
comment
-1 Это неправильный ответ. Если вы замените деление оператором по модулю, вы получите количество разделов batchSize, которое больше подходит для этого потока stackoverflow.com/questions/438188/ - person nawfal; 06.12.2012
comment
Я не согласен. Если вы используете по модулю, то вы распределяете элементы по группам по остатку. Вы получите элемент 0 в группе 0, элемент 1 в группе 1 и т. Д. Это полностью изменит их порядок. Использование целочисленного деления, как я сделал здесь, означает, что элементы 0-2 входят в группу 0, 3-5 входят в группу 1 и т. Д. Я считаю, что это то, что было задумано. Если вы согласны, пожалуйста, удалите -1 из моего ответа. - person Mel; 06.12.2012
comment
ты прав. Я пропустил часть ToList, которая фактически выполняет запрос перед уходом. Почему бы не использовать более легкий ToArray? Мне нужно отредактировать ваш ответ, чтобы убрать мой -1. +1 тоже! :) - person nawfal; 06.12.2012
comment
ToList - это как раз то, что я использовал в то время, но ToArray должен работать нормально. - person Mel; 07.12.2012

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;  
    }  
}
person Zotta    schedule 22.09.2010

Простейшее решение дает Мел:

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 операции здесь

person nawfal    schedule 06.12.2012
comment
ох, это намного быстрее! хотя stackoverflow.com/a/4835541/1037948 начал вытеснять вас после пары запусков в linqpad ...;) - person drzaus; 11.07.2014
comment
@drzaus имейте в виду, что этот ответ - опасная конструкция с побочными эффектами. Поскольку внутреннее и внешнее выполняется в одном и том же перечислителе, вы получаете неожиданные результаты. Если вы перечисляете только внешнюю последовательность, внутренние последовательности больше не действительны; или вы не можете повторять внутренние последовательности дважды. Или просто попробуйте var x = returnedResult.ElementAt(n).ToList();, вы получите неожиданный результат. - person nawfal; 11.07.2014
comment
Поэтому я недавно снова попытался выполнить несколько сравнений производительности (тесты + результаты здесь) между простым вызовом этого метода и выполнением каких-либо действий с результатами, и это было не так быстро по сравнению с некоторыми другими предложениями. Я что-то упускаю? - person drzaus; 25.07.2014
comment
@drzaus, это было замечательно, что вы выполнили этот тест. Я посмотрю на вашу ссылку через неделю. Мне нужно время. Спасибо :) дам знать. - person nawfal; 26.07.2014
comment
Недавно реализованное «простое» решение приводит к дублированию перечисления / оценки - см. Объяснение gist.github.com/zaus / 4cae1a5b42475a083287, что, вероятно, также указано в уже отвеченном вопросе - person drzaus; 10.06.2015
comment
Я все это увижу. Спасибо. Дай мне немного времени. - person nawfal; 10.06.2015

person    schedule
comment
+1 Но обратите внимание, что эта группа группируется каждый 8-й элемент, т.е. вы получаете {0,8,16}, {1,9,17}, ... - person Ruben Bartelink; 13.11.2012
comment
Это больше о разделении, чем о разделении - более подходящий здесь - person nawfal; 06.12.2012
comment
Кроме того, это может дать пустые внутренние IEnumerables, если число 8 больше, чем количество элементов в items, что может быть, а может и не быть желательным. Я все равно включил ваш ответ в другую ветку .. - person nawfal; 06.12.2012