最后加上非常有用的mhand评论
原始答案
尽管大多数解决方案可能有效,但我认为它们的效率不是很高。假设您只想要前几个块的前几个项目。那么你就不想遍历序列中的所有(无数)项目。
以下将最多列举两次:一次用于 Take,一次用于 Skip。它不会枚举比您将使用的元素更多的元素:
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 将获取 Enumerator,执行 1 MoveNext() 并在 Disposing Enumerator 后返回返回值。这将完成 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 个元素,那么 yield return 将被执行 M 次。这表示:
- 获取枚举器
- 调用 MoveNext() 和 Current M 次。
- 释放枚举器
在第一个块被 yield 返回后,我们跳过这个第一个块:
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;
}
}
}
如您所见,对 Chunk 中的每个元素SkipIterator
调用MoveNext()
一次。它不叫Current
。
因此,对于每个 Chunk,我们看到已完成以下操作:
- 任何():GetEnumerator;1 移动下一个();处置枚举器;
拿():
如果您查看枚举器发生的情况,您会发现有很多对 MoveNext() 的调用,并且只对Current
您实际决定访问的 TSource 项进行调用。
如果你取 N 个大小为 chunkSize 的块,则调用 MoveNext()
- Any() N 次
- 还没有任何时间 Take,只要你不枚举块
- Skip() 的 N 倍 chunkSize
如果您决定仅枚举每个获取的块的前 M 个元素,那么您需要对每个枚举的块调用 MoveNext M 次。
总数
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() 是否明智取决于您的存储库