最后加上非常有用的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);
根据reference source,这将执行以下操作:
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;
}
}
在您开始枚举获取的块之前,这并没有多大作用。如果您获取多个 Chunk,但决定不枚举第一个 Chunk,则不会执行 foreach,因为您的调试器会向您显示。
如果你决定取第一个块的前 M 个元素,那么 yield return 将被执行 M 次。这意味着:
- 获取枚举器
- 调用 MoveNext() 和 Current M 次。
- 配置枚举器
在第一个chunk被yield返回后,我们跳过这个第一个chunk:
source = source.Skip(chunkSize);
再次:我们将查看reference source 以找到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 为 Chunk 中的每个元素调用一次 MoveNext()。 它不会调用Current。
因此,我们看到每个 Chunk 都完成了以下操作:
- Any():GetEnumerator; 1 移动下一个();处置枚举器;
-
Take():
- 如果没有枚举块的内容,则什么都没有。
如果枚举内容:GetEnumerator(),每个枚举项一个 MoveNext 和一个 Current,Dispose 枚举器;
Skip():对于每个枚举的块(不是块的内容):
GetEnumerator(), MoveNext() chunkSize 次,没有 Current!配置枚举器
如果您查看枚举器发生的情况,您会发现有很多对 MoveNext() 的调用,并且仅对您实际决定访问的 TSource 项调用 Current。
如果你取 N 个大小为 chunkSize 的 Chunks,则调用 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() 是否明智,取决于您的存储库