最後に非常に役立つmhandのコメントの後の追加
元の答え
ほとんどの解決策はうまくいくかもしれませんが、私は非常に効率的ではないと思います。最初のいくつかのチャンクの最初のいくつかのアイテムだけが必要であると仮定します。次に、シーケンス内のすべての(ジリオン)アイテムを反復処理する必要はありません。
以下は、最大で2回列挙されます。1回はテイク、もう1回はスキップです。使用するよりも多くの要素を列挙しません。
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回呼び出します。
- 列挙子を破棄する
最初のチャンクが生成されて返された後、この最初のチャンクをスキップします。
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;
}
}
}
ご覧のとおり、チャンク内のすべての要素に対して1回ずつSkipIterator
呼び出しますMoveNext()
。それは呼びませんCurrent
。
したがって、チャンクごとに、次のことが行われていることがわかります。
- Any():GetEnumerator; 1 MoveNext(); 列挙子を破棄します。
取る():
列挙子で何が発生するかを見ると、MoveNext()への呼び出しが多く、Current
実際にアクセスすることに決めたTSource項目への呼び出しのみであることがわかります。
サイズchunkSizeのN個のチャンクを取得する場合は、MoveNext()を呼び出します。
- Any()のN回
- チャンクを列挙しない限り、まだテイクの時間はありません
- 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はリストであるオブジェクトを返し、DbContextはデータがすでにフェッチされていることを記憶しています。
チャンクで項目の分割を開始する前にAsEnumerable()またはToLists()を呼び出すのが賢明かどうかは、リポジトリによって異なります