将IEnumerable <T>拆分为固定大小的块(返回IEnumerable <IEnumerable <T >>,其中内部序列具有固定长度)

Ala*_*Maw 44 c# linq ienumerable

我想把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;
}
Run Code Online (Sandbox Code Playgroud)

有没有更惯用的东西?

编辑:虽然这已被标记为Divide数组的副本到子序列数组的数组,但它不是 - 该问题涉及拆分数组,而这是关于IEnumerable<T>.此外,该问题要求填充最后一个子序列.这两个问题密切相关,但并不相同.

tak*_*gen 62

您可以尝试自己实现上面提到的Batch方法,如下所示:

    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; 
        } 
    }
Run Code Online (Sandbox Code Playgroud)

我从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)}]");
    }
}
Run Code Online (Sandbox Code Playgroud)

将输出:

[2, 3, 4, 5, 6] //only first element is skipped.
[7, 8, 9, 10, 11]
[12, 13, 14, 15, 16]
[17, 18, 19, 20]
Run Code Online (Sandbox Code Playgroud)

因此,如果您使用案例假设批量顺序评估批次,那么上面的懒惰解决方案将起作用,否则如果您不能保证严格顺序批处理(例如,当您想要并行处理批次)时,您可能需要一个解决方案它急切地列举了批量内容,类似于上面的问题或MoreLINQ中提到的内容

  • 它是马车.如果你在第一批之前枚举第二批,你会得到错误的结果! (6认同)
  • 它太多了因为太懒了:) (5认同)
  • 这种实现的副作用 - 巨大的劣势(http://blogs.msdn.com/b/pfxteam/archive/2012/11/16/plinq-and-int32-maxvalue.aspx).对我来说 - 它会产生非常意外的无效输出.Jeppe Stig Nielsen的演绎.- 是最好的! (4认同)
  • @MBoros,它不是马车.这是一种性能可靠性权衡.如果你只需要将"IEnumerable"(假设无限!)分成批次,它就可以完美地完成工作.如果你需要以随机顺序枚举顶级或重新迭代,你可以使用其他实现(例如http://stackoverflow.com/a/438513/947012)但是它们会在内存中创建额外的对象并不是那么快. (3认同)
  • 这个实现对我来说似乎很好(正如一些用户所说,它是性能的折衷).但是,如果有人在完全枚举前一个分区之前尝试枚举分区,它应该抛出一个异常(例如:在并行上下文中使用时).我正在考虑类似于当您尝试修改当前在foreach循环中枚举的集合时存在的保护(该异常可防止获取不正确的数据). (3认同)
  • @greatvovan 抱歉。我对“不是越野车”这个词很感兴趣。这是地狱般的越野车。但当然在一些特殊情况下它也可以给出正确的结果。我永远不会使用有错误的代码来指望幸运的情况,但这是你个人的选择。无论如何,你不应该在 SO 上宣传这个解决方案,因为它是不正确的! (2认同)
  • @MBoros你在说什么运气?你的教育是计算机科学还是深奥的?它是一种确定性算法,它在100%的情况下适用于适当的输入和使用.假设IEnumerable表示来自某个外部队列的项目.你怎么能算出批次数呢?更重要的是 - 为了什么?任务是将序列分成块,就是这样.您现在正在引入其他要求并抱怨它不起作用. (2认同)
  • @MBoros基本上你没能为这个实现编写正确的单元测试. (2认同)

L.B*_*L.B 13

也许?

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));
}
Run Code Online (Sandbox Code Playgroud)

还有一个已经实现的:morelinq的Batch.

  • -1,因为这个在返回任何结果之前将所有内容都拉入内存,然后通过在哈希表中对事物进行分组来使用更多内存. (9认同)

Jep*_*sen 13

感觉就像你想要两个迭代器块(" 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();      
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 在返回的项目上执行ToList()会忽略整个问题的要点...... (3认同)
  • 这真的是最好的解决方案!!!我试过很多!原因:无副作用(参见 http://blogs.msdn.com/b/pfxteam/archive/2012/11/16/plinq-and-int32-maxvalue.aspx),懒惰/流媒体,快速和内存效率。 (2认同)
  • @SalientBrain 由于每个批次都会调用“ToList”,因此如果批次很大,内存效率会受到一定影响。这是我见过的最好的解决方案。不幸的是,我认为不可能有一个_完全_流式解决方案(即,批次和每批中的项目都进行流式传输的解决方案)。 (2认同)
  • 我意识到这已经有4年了,但用LINQ的`Take`和`Skip`扩展方法替换这个实现的一个很好的部分是否合适? (2认同)

Ser*_*kov 7

最疯狂的解决方案(使用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
}
Run Code Online (Sandbox Code Playgroud)

我知道我改变了签名,但无论如何我们都知道我们会将一些固定大小的集合作为一个块.

顺便说一句,如果您将使用迭代器块,请不要忘记将您的实现分成两个方法来急切地验证参数!


Til*_*lak 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
Run Code Online (Sandbox Code Playgroud)