Разбейте 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;
}

Есть ли что-то более идиоматическое?

РЕДАКТИРОВАТЬ: Хотя это было помечено как дубликат массива делить на массив массива подпоследовательности, это не так - этот вопрос касается разделения массива, в то время как это о IEnumerable<T>, Кроме того, этот вопрос требует дополнения последней подпоследовательности. Эти два вопроса тесно связаны, но не совпадают.

8 ответов

Решение

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

    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]

Таким образом, если вы используете case, предполагающий пакетную обработку при последовательной оценке пакетов, то сработает ленивое решение, описанное выше, в противном случае, если вы не можете гарантировать строго последовательную пакетную обработку (например, если вы хотите обрабатывать пакеты параллельно), вам, вероятно, понадобится решение. который охотно перечисляет пакетное содержимое, подобное тому, которое упоминалось в вопросе выше или в MoreLINQ

Такое ощущение, что вы хотите два блока итератора ("yield return методы "). Я написал этот метод расширения:

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();      
    }
  }
}

Может быть?

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.

Самое безумное решение (с 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
}

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

Кстати, если вы будете использовать блок итератора, не забудьте разделить вашу реализацию на два метода, чтобы с готовностью проверить аргументы!

Для элегантного решения, вы также можете взглянуть на 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
public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> items, 
                                                       int partitionSize)
{
    int i = 0;
    return items.GroupBy(x => i++ / partitionSize).ToArray();
}

Вы можете сделать это, используя перегрузкуEnumerable.GroupBy и воспользоваться преимуществом целочисленного деления.

return items.Select((element, index) => new { Element = element, Index = index })
    .GroupBy(obj => obj.Index / partitionSize, (_, partition) => partition);

Как насчет классов разделителей в пространстве имен System.Collections.Concurrent?

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