补充后很有用的评论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将获得枚举器,执行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;
}
}
在开始枚举所获取的Chunk之前,这不会做很多工作。如果您获取了几个Chunk,但决定不对第一个Chunk进行枚举,则不会执行foreach,正如调试器将显示的那样。
如果你决定取第一个块的前M个元素,那么yield返回将执行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中,我们看到完成了以下工作:
任何():GetEnumerator;1 MoveNext ();处理枚举器;
带():
如果块的内容没有被枚举,则什么都没有。
如果内容被枚举:GetEnumerator(),每个枚举项一个MoveNext和一个Current, Dispose枚举器;
Skip():对于每个被枚举的块(不是块的内容):
GetEnumerator(), MoveNext() chunkSize times, no Current!处理枚举器
如果您观察枚举器发生了什么,您将看到有很多对MoveNext()的调用,而对于您实际决定访问的TSource项,只有对Current的调用。
如果你取N个大小为chunkSize的chunk,然后调用MoveNext()
Any() N次
只要你不枚举块,Take就没有时间了
N倍chunkSize for Skip()
如果您决定只枚举每个获取块的前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()是否明智取决于您的存储库