最后添加了对mhand的非常有用的评论
原始答案
尽管大多数解决方案都可以使用,但我认为它们并不是非常有效。假设您只需要前几个块中的前几个项目。然后,您将不需要遍历序列中的所有(成千上万个)项目。
以下内容将最多列举两次:一次用于“汇整”,一次用于“跳过”。它不会枚举超过您将使用的元素:
public static IEnumerable<IEnumerable<TSource>> ChunkBy<TSource>
(this IEnumerable<TSource> source, int chunkSize)
{
while (source.Any()) // while there are elements left
{ // still something to chunk:
yield return source.Take(chunkSize); // return a chunk of chunkSize
source = source.Skip(chunkSize); // skip the returned chunk
}
}
这将枚举序列多少次?
假设您将来源划分为chunkSize
。您只枚举前N个块。从每个枚举的块中,您只会枚举前M个元素。
While(source.Any())
{
...
}
Any将获得枚举数,执行1 MoveNext()并在处理枚举数后返回返回值。这将完成N次
yield return source.Take(chunkSize);
根据参考资料,这将执行以下操作:
public static IEnumerable<TSource> Take<TSource>(this IEnumerable<TSource> source, int count)
{
return TakeIterator<TSource>(source, count);
}
static IEnumerable<TSource> TakeIterator<TSource>(IEnumerable<TSource> source, int count)
{
foreach (TSource element in source)
{
yield return element;
if (--count == 0) break;
}
}
直到您开始对提取的块进行枚举之前,这并不会起作用。如果获取多个块,但决定不对第一个块进行枚举,则不会执行foreach,因为调试器将向您显示。
如果决定采用第一个块的前M个元素,则收益率返回将精确执行M次。这表示:
- 获取枚举器
- 调用MoveNext()和当前M次。
- 配置枚举器
返回第一个块后,我们跳过此第一个块:
source = source.Skip(chunkSize);
再次:我们将参考参考资料来查找skipiterator
static IEnumerable<TSource> SkipIterator<TSource>(IEnumerable<TSource> source, int count)
{
using (IEnumerator<TSource> e = source.GetEnumerator())
{
while (count > 0 && e.MoveNext()) count--;
if (count <= 0)
{
while (e.MoveNext()) yield return e.Current;
}
}
}
如您所见,块中的每个元素都SkipIterator
调用MoveNext()
一次。它不打电话Current
。
因此,根据块,我们看到完成了以下操作:
- Any():GetEnumerator;1 MoveNext(); 设置枚举器;
采取():
如果查看枚举数会发生什么,您会看到对MoveNext()的调用很多,只有Current
实际决定访问的TSource项的调用。
如果采用大小为块大小的N个块,则调用MoveNext()
- N次Any()
- 只要您不枚举区块,现在还没有时间进行Take
- Skip()的N倍chunkSize
如果您决定只枚举每个获取的块的前M个元素,则需要为每个枚举的块调用M次MoveNext。
总数
MoveNext calls: N + N*M + N*chunkSize
Current calls: N*M; (only the items you really access)
因此,如果您决定枚举所有块的所有元素:
MoveNext: numberOfChunks + all elements + all elements = about twice the sequence
Current: every item is accessed exactly once
MoveNext是否需要大量工作,取决于源序列的类型。对于列表和数组,这是一个简单的索引增量,可能超出范围检查。
但是,如果IEnumerable是数据库查询的结果,请确保在计算机上确实实现了数据,否则将多次提取数据。DbContext和Dapper将在将其访问之前将数据正确传输到本地进程。如果多次枚举相同的序列,则不会多次获取。Dapper返回一个对象,该对象是一个List,DbContext记住该数据已被获取。
在开始在块中划分项目之前,是否明智地调用AsEnumerable()或ToLists()取决于您的存储库