在linq中创建批次

Bla*_*keH 84 c# linq

有人可以建议在linq中创建一定大小的批次吗?

理想情况下,我希望能够以一些可配置的数量块执行操作.

Ser*_*kiy 94

您不需要编写任何代码.使用MoreLINQ Batch方法,将源序列批量化为大小的存储桶(MoreLINQ可用作您可以安装的NuGet包):

int size = 10;
var batches = sequence.Batch(size);
Run Code Online (Sandbox Code Playgroud)

实现方式如下:

public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
                  this IEnumerable<TSource> source, int size)
{
    TSource[] bucket = null;
    var count = 0;

    foreach (var item in source)
    {
        if (bucket == null)
            bucket = new TSource[size];

        bucket[count++] = item;
        if (count != size)
            continue;

        yield return bucket;

        bucket = null;
        count = 0;
    }

    if (bucket != null && count > 0)
        yield return bucket.Take(count).ToArray();
}
Run Code Online (Sandbox Code Playgroud)

  • 这是一个非常好的解决方案.在现实生活中你:验证用户输入,将批处理视为整个项目集合(无论如何累积项目),并且经常并行处理批处理(迭代器方法不支持,除非你知道,否则将是一个令人讨厌的惊喜实施细节). (8认同)
  • @NickWhaley很好,同意你将分配额外的空间,但在现实生活中,你通常只有相反的情况 - 1000个项目的列表应该分批50个:) (7认同)
  • 我并不是要冒犯你,但有一些简单的解决方案根本不会积累.此外,即使对于不存在的元素,这也将分配空间:`Batch(new int [] {1,2},1000000)` (4认同)
  • 每个项目4个字节执行*非常*?你有一些测试显示*非常*意味着什么吗?如果您将数百万个项目加载到内存中,那么我就不会这样做.使用服务器端分页 (3认同)

L.B*_*L.B 77

public static class MyExtensions
{
    public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> items,
                                                       int maxItems)
    {
        return items.Select((item, inx) => new { item, inx })
                    .GroupBy(x => x.inx / maxItems)
                    .Select(g => g.Select(x => x.item));
    }
}
Run Code Online (Sandbox Code Playgroud)

用法是:

List<int> list = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

foreach(var batch in list.Batch(3))
{
    Console.WriteLine(String.Join(",",batch));
}
Run Code Online (Sandbox Code Playgroud)

OUTPUT:

0,1,2
3,4,5
6,7,8
9
Run Code Online (Sandbox Code Playgroud)

  • 一旦`GroupBy`开始枚举,它是否必须完全枚举其源?这失去了对源的懒惰评估,因此,在某些情况下,批处理的所有好处! (12认同)
  • 这样做 - 当您需要将现有的事物块分解为较小的批量事物以进行高性能处理时,它是完全合适的。另一种方法是一个粗略的查找循环,您可以手动分解批次,但仍然遍历整个源代码。 (3认同)
  • 正如@ErikE所提到的,这个方法完全枚举了它的来源,所以尽管它看起来不错,但却违背了懒惰评估/流水线的目的 (2认同)

Nic*_*ley 27

以上所有都是大批量或低内存空间的执行.不得不写我自己的管道(注意没有任何项目积累):

public static class BatchLinq {
    public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size) {
        if (size <= 0)
            throw new ArgumentOutOfRangeException("size", "Must be greater than zero.");

        using (IEnumerator<T> enumerator = source.GetEnumerator())
            while (enumerator.MoveNext())
                yield return TakeIEnumerator(enumerator, size);
    }

    private static IEnumerable<T> TakeIEnumerator<T>(IEnumerator<T> source, int size) {
        int i = 0;
        do
            yield return source.Current;
        while (++i < size && source.MoveNext());
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑:此方法的已知问题是,在移动到下一批之前,必须完全枚举和枚举每个批次.例如,这不起作用:

//Select first item of every 100 items
Batch(list, 100).Select(b => b.First())
Run Code Online (Sandbox Code Playgroud)

  • @neontapir仍然.一个硬币分拣机,首先给你镍,然后硬币,必须首先检查每一枚硬币,然后给你一毛钱,以确保没有更多的镍. (2认同)
  • 啊啊哈哈,当我抓住这段代码时,错过了你的编辑笔记.花了一些时间来理解为什么迭代未枚举的批次实际上枚举了整个原始集合(!!!),提供X批次,每个都有枚举1个项目(其中X是原始集合项目的数量). (2认同)
  • @NickWhaley 如果我通过您的代码在 IEnumerable&lt;IEnumerable&lt;T&gt;&gt; 结果上执行 Count() ,它会给出错误的答案,它会给出元素总数,而预期是创建的批次总数。MoreLinq 批处理代码不是这种情况 (2认同)

Mat*_*dge 26

如果你从sequence定义为a 开始IEnumerable<T>,并且你知道它可以安全地多次枚举(例如,因为它是一个数组或一个列表),你可以使用这个简单的模式来批量处理元素:

while (sequence.Any())
{
    var batch = sequence.Take(10);
    sequence = sequence.Skip(10);

    // do whatever you need to do with each batch here
}
Run Code Online (Sandbox Code Playgroud)

  • @DevHawk:是的.但是,请注意,(大部分(r))集合的[性能将呈指数级增长](https://www.make-awesome.com/2010/08/batch-or-partition-a-collection-with-linq/). (4认同)
  • 很好,简单的批处理方式,无需代码或需要外部库 (2认同)

inf*_*lch 15

这是Batch的完全惰性,低开销,单功能实现,不进行任何累积.在EricRoller的帮助下,基于(并修复了)Nick Whaley的解决方案.

迭代直接来自底层IEnumerable,因此元素必须按严格顺序枚举,并且访问不超过一次.如果内部循环中没有使用某些元素,则会丢弃它们(并尝试通过保存的迭代器再次访问它们InvalidOperationException: Enumeration already finished.).

您可以在.NET Fiddle上测试完整的示例.

public static class BatchLinq
{
    public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
    {
        if (size <= 0)
            throw new ArgumentOutOfRangeException("size", "Must be greater than zero.");
        using (var enumerator = source.GetEnumerator())
            while (enumerator.MoveNext())
            {
                int i = 0;
                // Batch is a local function closing over `i` and `enumerator` that
                // executes the inner batch enumeration
                IEnumerable<T> Batch()
                {
                    do yield return enumerator.Current;
                    while (++i < size && enumerator.MoveNext());
                }

                yield return Batch();
                while (++i < size && enumerator.MoveNext()); // discard skipped items
            }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 这是这里唯一完全懒惰的实现.与python itertools.GroupBy实现一致. (2认同)

Mon*_*Zhu 13

我想知道为什么没有人发布过老式的 for 循环解决方案。这是一个:

List<int> source = Enumerable.Range(1,23).ToList();
int batchsize = 10;
for (int i = 0; i < source.Count; i+= batchsize)
{
    var batch = source.Skip(i).Take(batchsize);
}
Run Code Online (Sandbox Code Playgroud)

这种简单性是可能的,因为 Take 方法:

... 枚举source并产生元素,直到count元素被产生或不source包含更多元素。如果count超过 中的元素数sourcesource则返回 中的所有元素

免责声明:

在循环内使用 Skip 和 Take 意味着可枚举将被枚举多次。如果可枚举被延迟,这是危险的。它可能会导致多次执行数据库查询、Web 请求或文件读取。此示例明确用于未延迟的 List 的使用,因此问题不大。这仍然是一个缓慢的解决方案,因为每次调用时都会枚举集合。

这也可以使用该GetRange方法解决,但需要额外的计算来提取可能的剩余批次:

for (int i = 0; i < source.Count; i += batchsize)
{
    int remaining = source.Count - i;
    var batch = remaining > batchsize  ? source.GetRange(i, batchsize) : source.GetRange(i, remaining);
}
Run Code Online (Sandbox Code Playgroud)

这是处理此问题的第三种方法,它适用于 2 个循环。这确保了该集合仅被枚举 1 次!:

int batchsize = 10;
List<int> batch = new List<int>(batchsize);

for (int i = 0; i < source.Count; i += batchsize)
{
    // calculated the remaining items to avoid an OutOfRangeException
    batchsize = source.Count - i > batchsize ? batchsize : source.Count - i;
    for (int j = i; j < i + batchsize; j++)
    {
        batch.Add(source[j]);
    }           
    batch.Clear();
}
Run Code Online (Sandbox Code Playgroud)

  • 在循环内使用“Skip”和“Take”意味着可枚举对象将被枚举多次。如果可枚举被延迟,这是危险的。它可能会导致多次执行数据库查询、Web 请求或文件读取。在您的示例中,您有一个未延迟的“List”,因此问题不大。 (7认同)
  • 非常好的解决方案。人们忘记了如何使用 for 循环 (2认同)

dan*_*ana 7

Enumerable.Chunk()延伸方法是目前在预览和被提名被添加到.NET 6.0。

例子:

var list = new List<int> { 1, 2, 3, 4, 5, 6, 7 };

var chunks = list.Chunk(3);
// returns { { 1, 2, 3 }, { 4, 5, 6 }, { 7 } }
Run Code Online (Sandbox Code Playgroud)

对于那些迫不及待的人,可以在 GitHub 上找到源代码