在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会获取Enumerator,执行1次MoveNext()并在释放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;
}
}
}
正如您所看到的,
SkipIterator
对于 Chunk 中的每个元素都调用了一次
MoveNext()
,但没有调用
Current
。
因此对于每个 Chunk,我们可以看到以下操作:
- Any(): GetEnumerator;1 次 MoveNext();Dispose Enumerator
- Take():
- 如果 Chunk 的内容未被枚举,则不执行任何操作。
- 如果 Chunk 内容被枚举:GetEnumerator(),针对每个枚举项执行一次 MoveNext 和一次 Current,然后 Dispose enumerator。
- Skip():对于每个已枚举的 Chunk(而不是每个 Chunk 的内容):
- GetEnumerator();
- MoveNext() chunkSize 次,没有 Current!
- Dispose enumerator
如果您查看枚举器运行的情况,就会发现有很多次调用 MoveNext(),而只有在您要访问 TSource 项时才会调用
Current
。
如果您取 N 个大小为 chunkSize 的 Chunk,则需要调用 MoveNext() 次数如下:
- 对于 Any(),需要调用 N 次;
- 对于 Take(),只要您不枚举 Chunk,就不需要调用任何次数;
- 对于 Skip(),需要调用 N × chunkSize 次。
如果您决定只枚举每个获取的 Chunk 的前 M 个元素,则需要针对每个枚举的 Chunk 调用 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()是否明智取决于您的存储库。