在Linq中创建批次


104

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

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

Answers:


116

您无需编写任何代码。使用MoreLINQ Batch方法,该方法将源序列批处理成大小的存储桶(MoreLINQ可作为NuGet包提供,您可以安装):

int size = 10;
var batches = sequence.Batch(size);

实现为:

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

3
每个项目4个字节的执行情况非常糟糕?你有一些测试,这表明什么可怕的手段?如果您要将数百万个项目加载到内存中,那么我不会这样做。使用服务器端分页
Sergey Berezovskiy 2013年

4
我并不是要冒犯您,但是有一些简单的解决方案根本无法累积。此外,这甚至可以为不存在的元素分配空间:Batch(new int[] { 1, 2 }, 1000000)
Nick Whaley 2013年

7
@NickWhaley好吧,同意您的意见,即会分配额外的空间,但是在现实生活中,您通常会遇到相反的情况-1000件物品的清单应以50件为批次:)
Sergey Berezovskiy

1
是的,情况通常应该是相反的,但是在现实生活中,这些可能是用户输入。
Nick Whaley 2013年

8
这是一个完美的解决方案。在现实生活中,您:验证用户输入,将批处理视为项的完整集合(无论如何都会累积项),并经常并行处理批处理(迭代器方法不支持,除非您知道该批处理,否则这将是一个令人讨厌的惊喜)实施细节)。
Michael Petito 2014年

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

用法是:

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

输出:

0,1,2
3,4,5
6,7,8
9

对我来说

16
一旦GroupBy开始枚举,就不必完全枚举​​其来源吗?这样就失去了对源的懒惰评估,因此在某些情况下还失去了批处理的所有好处!
ErikE 2015年

1
哇,谢谢,您让我免于精神错乱。效果非常好
Riaan de Lange

3
正如@ErikE所提到的,此方法完全枚举了其来源,因此,尽管它看起来不错,但却无法达到延迟评估/管道化的目的
lasseschou 2016年

1
这样做-当您需要将现有的东西分解成小批的东西以进行高性能处理时,这是完全合适的。另一种选择是粗略的寻找循环,您可以在其中手动拆分批次,并仍然遍历整个源。
StingyJack

31

如果以sequence定义为开头IEnumerable<T>,并且知道可以安全地进行多次枚举(例如,因为它是数组或列表),则可以使用以下简单模式来批量处理元素:

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

    // do whatever you need to do with each batch here
}

2
不错的简单方法,无需大量代码即可批处理或需要外部库
DevHawk

5
@DevHawk:是的。但是请注意,在big(r)集合上,性能将成倍下降
RobIII

28

以上所有这些在大批处理或低内存空间下均表现出色。不得不写我自己的管道(注意任何地方都没有项目堆积):

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

编辑:此方法的已知问题是,在移至下一个批次之前,必须对每个批次进行完整的枚举和枚举。例如,这不起作用:

//Select first item of every 100 items
Batch(list, 100).Select(b => b.First())

1
上面发布的例程@LB也不执行项目累积。
neontapir

2
@neontapir还是。一台硬币分拣机,该硬币分拣机首先要给您镍币,然后将钱币硬币给硬币,必须首先检查每一个硬币,然后再给您一角硬币以确保没有更多的镍币。
Nick Whaley 2013年

2
啊啊啊,我抓到这段代码时,错过了您的编辑笔记。花了一些时间来理解为什么要遍历未枚举的批次实际上枚举了整个原始集合(!!!),提供了X个批次,每个批次都枚举了1个项目(其中X是原始收集项目的数量)。
eli 2014年

2
@NickWhaley如果我通过您的代码对IEnumerable <IEnumerable <T >>结果执行Count(),则给出错误的答案,给出元素的总数,这是预期的总批数。MoreLinq批处理代码不是这种情况
Mrinal Kamboj


24

这是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
            }
    }
}

2
这是这里唯一的完全懒惰的实现。与python itertools.GroupBy实现一致。
埃里克·罗勒

1
您可以done通过始终调用e.Count()after 来取消检查yield return e。您将需要重新排列BatchInner中的循环,以不调用source.Currentif 定义的行为i >= size。这将消除BatchInner为每个批次分配新的需求。
埃里克·罗勒

1
没错,您仍然需要捕获有关每个批处理进度的信息。如果您尝试从每个批次中获取第二项,我确实在您的代码中发现了一个错误:bug fiddle。没有单独类的固定实现(使用C#7)在此处:fixed fiddle。请注意,我希望CLR仍会在每个循环中创建一次局部函数来捕获变量,i因此这不一定比定义一个单独的类更有效,但我认为它会更干净一些。
埃里克·罗勒

1
我使用BenchmarkDotNet针对System.Reactive.Linq.EnumerableEx.Buffer对这个版本进行了基准测试,您的实现速度提高了3-4,有安全隐患。在内部,EnumerableEx.Buffer分配列表的队列<T> github.com/dotnet/reactive/blob/...
约翰Zabroski

1
如果需要此版本的缓冲版本,则可以执行以下操作:公共静态IEnumerable <IReadOnlyList <T >> BatchBuffered <T>(此IEnumerable <T>源,整数大小)=> Batch(源,大小)。 >(IReadOnlyList <T>)chunk.ToList()); 使用IReadOnlyList <T>可以提示用户缓存了输出。您也可以保留IEnumerable <IEnumerable <T >>。
gfache

11

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

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

之所以可以如此简单是因为Take方法:

...枚举source并产生元素,直到产生了元素countsource不再包含其他元素为止。如果count超过中的元素数sourcesource则返回的所有元素

免责声明:

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

这也可以使用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);
}

这是处理此问题的第三种方法,可用于2个循环。这样可以确保仅对集合进行一次枚举!:

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

2
非常好的解决方案。人们忘记了如何使用循环
VitalickS

1
在循环内部使用SkipTake意味着可枚举将被多次枚举。如果枚举数被延迟,这将很危险。它可能导致数据库查询,Web请求或文件读取的多次执行。在您的示例中,您有一个List不延迟的,因此问题不大。
Theodor Zoulias

@TheodorZoulias是的,我知道,这实际上就是我今天发布第二个解决方案的原因。我将您的评论发布为免责声明,因为您的措辞很好,我可以引用您吗?
旺朱

我编写了带有2个循环的第三个解决方案,以便仅对集合进行一次枚举。skip.take是一个非常低效的解决方案
Mong Zhu

4

与MoreLINQ相同的方法,但是使用List而不是Array。我还没有做过基准测试,但是可读性对某些人来说更重要:

    public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
    {
        List<T> batch = new List<T>();

        foreach (var item in source)
        {
            batch.Add(item);

            if (batch.Count >= size)
            {
                yield return batch;
                batch.Clear();
            }
        }

        if (batch.Count > 0)
        {
            yield return batch;
        }
    }

1
您不应该重复使用批处理变量。您的消费者可能因此而完全陷入困境。另外,将size参数传递给您new List以优化其大小。
ErikE

1
轻松解决:替换batch.Clear();batch = new List<T>();
NetMage

3

这是Nick Whaley(link)和infogulch(link)惰性Batch实现的尝试改进。这是严格的。您要么以正确的顺序枚举批处理,要么得到异常。

public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
    this IEnumerable<TSource> source, int size)
{
    if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
    using (var enumerator = source.GetEnumerator())
    {
        int i = 0;
        while (enumerator.MoveNext())
        {
            if (i % size != 0) throw new InvalidOperationException(
                "The enumeration is out of order.");
            i++;
            yield return GetBatch();
        }
        IEnumerable<TSource> GetBatch()
        {
            while (true)
            {
                yield return enumerator.Current;
                if (i % size == 0 || !enumerator.MoveNext()) break;
                i++;
            }
        }
    }
}

这是Batchtype源的懒惰实现IList<T>。这一点对枚举没有任何限制。可以按任何顺序对批次进行部分枚举,并且可以多次枚举。但是,在枚举期间不修改集合的限制仍然存在。这是通过enumerator.MoveNext()在产生任何块或元素之前进行伪调用来实现的。不利的一面是枚举器没有处理,因为枚举何时结束尚不清楚。

public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
    this IList<TSource> source, int size)
{
    if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
    var enumerator = source.GetEnumerator();
    for (int i = 0; i < source.Count; i += size)
    {
        enumerator.MoveNext();
        yield return GetChunk(i, Math.Min(i + size, source.Count));
    }
    IEnumerable<TSource> GetChunk(int from, int toExclusive)
    {
        for (int j = from; j < toExclusive; j++)
        {
            enumerator.MoveNext();
            yield return source[j];
        }
    }
}

2

我很晚才加入,但是我发现了一些更有趣的东西。

因此,我们可以在这里使用它SkipTake获得更好的性能。

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

        public static IEnumerable<T> Batch2<T>(this IEnumerable<T> items, int skip, int take)
        {
            return items.Skip(skip).Take(take);
        }

    }

接下来,我检查了100000条记录。只有在以下情况下,循环才会花费更多时间Batch

控制台应用程序代码。

static void Main(string[] args)
{
    List<string> Ids = GetData("First");
    List<string> Ids2 = GetData("tsriF");

    Stopwatch FirstWatch = new Stopwatch();
    FirstWatch.Start();
    foreach (var batch in Ids2.Batch(5000))
    {
        // Console.WriteLine("Batch Ouput:= " + string.Join(",", batch));
    }
    FirstWatch.Stop();
    Console.WriteLine("Done Processing time taken:= "+ FirstWatch.Elapsed.ToString());


    Stopwatch Second = new Stopwatch();

    Second.Start();
    int Length = Ids2.Count;
    int StartIndex = 0;
    int BatchSize = 5000;
    while (Length > 0)
    {
        var SecBatch = Ids2.Batch2(StartIndex, BatchSize);
        // Console.WriteLine("Second Batch Ouput:= " + string.Join(",", SecBatch));
        Length = Length - BatchSize;
        StartIndex += BatchSize;
    }

    Second.Stop();
    Console.WriteLine("Done Processing time taken Second:= " + Second.Elapsed.ToString());
    Console.ReadKey();
}

static List<string> GetData(string name)
{
    List<string> Data = new List<string>();
    for (int i = 0; i < 100000; i++)
    {
        Data.Add(string.Format("{0} {1}", name, i.ToString()));
    }

    return Data;
}

花费的时间是这样的。

首先-00:00:00.0708,00:00:00.0660

第二(跳一跳)-00:00:00.0008,00:00:00.0008


1
GroupBy在产生单行之前完全枚举。这不是进行批处理的好方法。
ErikE

@ErikE这取决于您要实现的目标。如果批处理不是问题,那么您只需要将项目分成较小的块进行处理就可以了。我用这对MSCRM中可能有100条记录这是LAMBDA批次没问题..其花几秒钟的储蓄..
JensB

1
当然,在某些情况下完整枚举无关紧要。但是,当您可以编写出色的实用方法时,为什么还要编写第二类实用程序方法呢?
ErikE

一个很好的选择,但与第一次返回的列表不同,它允许您循环浏览。
Gareth Hopkins

更改foreach (var batch in Ids2.Batch(5000))var gourpBatch = Ids2.Batch(5000)并检查定时结果。或将清单添加到var SecBatch = Ids2.Batch2(StartIndex, BatchSize);我,如果您的计时结果发生变化,我将很感兴趣。
Seabizkit,

2

因此,戴上功能齐全的帽子似乎并不重要。...但是在C#中,存在一些重大缺点。

您可能会认为这是IEnumerable的展开图(在Google上它可能会出现在某些Haskell文档中,但是如果使用F#,可能会有一些F#东西在使用,如果您知道F#,则在Haskell文档中一下,它将使感)。

展开与折叠(“聚合”)有关,除了展开而不是遍历输入IEnumerable之外,它遍历输出数据结构(它在IEnumerable和IObservable之间的相似关系,实际上我认为IObservable确实实现了称为“生成”的“展开”。 ..)

无论如何,首先您需要一个展开的方法,我认为这是可行的(不幸的是,它最终会炸掉大型“列表”的堆栈……您可以使用yield而不是concat在F#中安全地编写它);

    static IEnumerable<T> Unfold<T, U>(Func<U, IEnumerable<Tuple<U, T>>> f, U seed)
    {
        var maybeNewSeedAndElement = f(seed);

        return maybeNewSeedAndElement.SelectMany(x => new[] { x.Item2 }.Concat(Unfold(f, x.Item1)));
    }

这有点晦涩难懂,因为C#并未实现某些功能性语言视为理所当然的事情...但是它基本上需要一个种子,然后生成IEnumerable中的下一个元素和下一个种子的“也许”答案(也许在C#中不存在,因此我们使用IEnumerable对其进行伪造),并将其余答案串联起来(我不能保证这样做的“ O(n?)”复杂性)。

完成之后,

    static IEnumerable<IEnumerable<T>> Batch<T>(IEnumerable<T> xs, int n)
    {
        return Unfold(ys =>
            {
                var head = ys.Take(n);
                var tail = ys.Skip(n);
                return head.Take(1).Select(_ => Tuple.Create(tail, head));
            },
            xs);
    }

一切看起来都很干净……您将IEnumerable中的“ n”个元素作为“ next”元素,而“ tail”是未处理列表的其余部分。

如果头部中什么也没有...您已经结束了...您将返回“ Nothing”(但被伪造为一个空的IEnumerable>)...否则,您将head元素和尾部返回给处理。

您可能可以使用IObservable进行此操作,可能已经有一个类似“批处理”的方法,您可能可以使用它。

如果担心堆栈溢出的风险(可能应该),那么您应该在F#中实现(并且可能已经有了一些F#库(FSharpX?))。

(我仅对此进行了一些基本测试,因此其中可能存在奇怪的错误)。


1

我编写了一个自定义IEnumerable实现,该实现无需linq即可工作,并保证对数据进行单个枚举。它还可以完成所有这些操作,而无需备份列表或数组,这些列表或数组会导致大数据集上的内存爆炸。

以下是一些基本测试:

    [Fact]
    public void ShouldPartition()
    {
        var ints = new List<int> {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
        var data = ints.PartitionByMaxGroupSize(3);
        data.Count().Should().Be(4);

        data.Skip(0).First().Count().Should().Be(3);
        data.Skip(0).First().ToList()[0].Should().Be(0);
        data.Skip(0).First().ToList()[1].Should().Be(1);
        data.Skip(0).First().ToList()[2].Should().Be(2);

        data.Skip(1).First().Count().Should().Be(3);
        data.Skip(1).First().ToList()[0].Should().Be(3);
        data.Skip(1).First().ToList()[1].Should().Be(4);
        data.Skip(1).First().ToList()[2].Should().Be(5);

        data.Skip(2).First().Count().Should().Be(3);
        data.Skip(2).First().ToList()[0].Should().Be(6);
        data.Skip(2).First().ToList()[1].Should().Be(7);
        data.Skip(2).First().ToList()[2].Should().Be(8);

        data.Skip(3).First().Count().Should().Be(1);
        data.Skip(3).First().ToList()[0].Should().Be(9);
    }

用于对数据进行分区的扩展方法。

/// <summary>
/// A set of extension methods for <see cref="IEnumerable{T}"/>. 
/// </summary>
public static class EnumerableExtender
{
    /// <summary>
    /// Splits an enumerable into chucks, by a maximum group size.
    /// </summary>
    /// <param name="source">The source to split</param>
    /// <param name="maxSize">The maximum number of items per group.</param>
    /// <typeparam name="T">The type of item to split</typeparam>
    /// <returns>A list of lists of the original items.</returns>
    public static IEnumerable<IEnumerable<T>> PartitionByMaxGroupSize<T>(this IEnumerable<T> source, int maxSize)
    {
        return new SplittingEnumerable<T>(source, maxSize);
    }
}

这是实施班

    using System.Collections;
    using System.Collections.Generic;

    internal class SplittingEnumerable<T> : IEnumerable<IEnumerable<T>>
    {
        private readonly IEnumerable<T> backing;
        private readonly int maxSize;
        private bool hasCurrent;
        private T lastItem;

        public SplittingEnumerable(IEnumerable<T> backing, int maxSize)
        {
            this.backing = backing;
            this.maxSize = maxSize;
        }

        public IEnumerator<IEnumerable<T>> GetEnumerator()
        {
            return new Enumerator(this, this.backing.GetEnumerator());
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        private class Enumerator : IEnumerator<IEnumerable<T>>
        {
            private readonly SplittingEnumerable<T> parent;
            private readonly IEnumerator<T> backingEnumerator;
            private NextEnumerable current;

            public Enumerator(SplittingEnumerable<T> parent, IEnumerator<T> backingEnumerator)
            {
                this.parent = parent;
                this.backingEnumerator = backingEnumerator;
                this.parent.hasCurrent = this.backingEnumerator.MoveNext();
                if (this.parent.hasCurrent)
                {
                    this.parent.lastItem = this.backingEnumerator.Current;
                }
            }

            public bool MoveNext()
            {
                if (this.current == null)
                {
                    this.current = new NextEnumerable(this.parent, this.backingEnumerator);
                    return true;
                }
                else
                {
                    if (!this.current.IsComplete)
                    {
                        using (var enumerator = this.current.GetEnumerator())
                        {
                            while (enumerator.MoveNext())
                            {
                            }
                        }
                    }
                }

                if (!this.parent.hasCurrent)
                {
                    return false;
                }

                this.current = new NextEnumerable(this.parent, this.backingEnumerator);
                return true;
            }

            public void Reset()
            {
                throw new System.NotImplementedException();
            }

            public IEnumerable<T> Current
            {
                get { return this.current; }
            }

            object IEnumerator.Current
            {
                get { return this.Current; }
            }

            public void Dispose()
            {
            }
        }

        private class NextEnumerable : IEnumerable<T>
        {
            private readonly SplittingEnumerable<T> splitter;
            private readonly IEnumerator<T> backingEnumerator;
            private int currentSize;

            public NextEnumerable(SplittingEnumerable<T> splitter, IEnumerator<T> backingEnumerator)
            {
                this.splitter = splitter;
                this.backingEnumerator = backingEnumerator;
            }

            public bool IsComplete { get; private set; }

            public IEnumerator<T> GetEnumerator()
            {
                return new NextEnumerator(this.splitter, this, this.backingEnumerator);
            }

            IEnumerator IEnumerable.GetEnumerator()
            {
                return this.GetEnumerator();
            }

            private class NextEnumerator : IEnumerator<T>
            {
                private readonly SplittingEnumerable<T> splitter;
                private readonly NextEnumerable parent;
                private readonly IEnumerator<T> enumerator;
                private T currentItem;

                public NextEnumerator(SplittingEnumerable<T> splitter, NextEnumerable parent, IEnumerator<T> enumerator)
                {
                    this.splitter = splitter;
                    this.parent = parent;
                    this.enumerator = enumerator;
                }

                public bool MoveNext()
                {
                    this.parent.currentSize += 1;
                    this.currentItem = this.splitter.lastItem;
                    var hasCcurent = this.splitter.hasCurrent;

                    this.parent.IsComplete = this.parent.currentSize > this.splitter.maxSize;

                    if (this.parent.IsComplete)
                    {
                        return false;
                    }

                    if (hasCcurent)
                    {
                        var result = this.enumerator.MoveNext();

                        this.splitter.lastItem = this.enumerator.Current;
                        this.splitter.hasCurrent = result;
                    }

                    return hasCcurent;
                }

                public void Reset()
                {
                    throw new System.NotImplementedException();
                }

                public T Current
                {
                    get { return this.currentItem; }
                }

                object IEnumerator.Current
                {
                    get { return this.Current; }
                }

                public void Dispose()
                {
                }
            }
        }
    }

1

我知道每个人都使用复杂的系统来完成这项工作,但我真的不明白为什么。进行并跳过将允许使用带有Func<TSource,Int32,TResult>转换功能的公共选择功能进行所有这些操作。喜欢:

public IEnumerable<IEnumerable<T>> Buffer<T>(IEnumerable<T> source, int size)=>
    source.Select((item, index) => source.Skip(size * index).Take(size)).TakeWhile(bucket => bucket.Any());

2
这可能是非常低效的,因为给定的值source将经常被迭代。
凯文·梅尔

1
这不仅效率低下,而且还可能产生错误的结果。不能保证枚举两次时会产生相同的元素。拿这个枚举为例:Enumerable.Range(0, 1).SelectMany(_ => Enumerable.Range(0, new Random().Next()))
Theodor Zoulias '19

1

只是另一行实现。它甚至可以用于空列表,在这种情况下,您将获得零大小的批次集合。

var aList = Enumerable.Range(1, 100).ToList(); //a given list
var size = 9; //the wanted batch size
//number of batches are: (aList.Count() + size - 1) / size;

var batches = Enumerable.Range(0, (aList.Count() + size - 1) / size).Select(i => aList.GetRange( i * size, Math.Min(size, aList.Count() - i * size)));

Assert.True(batches.Count() == 12);
Assert.AreEqual(batches.ToList().ElementAt(0), new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
Assert.AreEqual(batches.ToList().ElementAt(1), new List<int>() { 10, 11, 12, 13, 14, 15, 16, 17, 18 });
Assert.AreEqual(batches.ToList().ElementAt(11), new List<int>() { 100 });

1

另一种方法是使用Rx Buffer运算符

//using System.Linq;
//using System.Reactive.Linq;
//using System.Reactive.Threading.Tasks;

var observableBatches = anAnumerable.ToObservable().Buffer(size);

var batches = aList.ToObservable().Buffer(size).ToList().ToTask().GetAwaiter().GetResult();

您永远不必使用GetAwaiter().GetResult()。这是强制调用异步代码的同步代码的代码味道。
gfache

-2
    static IEnumerable<IEnumerable<T>> TakeBatch<T>(IEnumerable<T> ts,int batchSize)
    {
        return from @group in ts.Select((x, i) => new { x, i }).ToLookup(xi => xi.i / batchSize)
               select @group.Select(xi => xi.x);
    }

在答案中添加一些描述/文字。在大多数情况下,仅放置代码可能意味着较少的意思。
Ariful Haque
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.