400 万人のユーザーの ID を格納する「IEnumerable users」を持つ C# プログラムを開発しています。別のメソッドでいくつかの操作を実行するには、毎回 IEnumerable をループしてバッチ 1000 の ID を抽出する必要があります。
IEnumerable の先頭から一度に 1000 個の ID を抽出し、何か別のことを行ってから、次の 1000 個のバッチを取得するにはどうすればよいですか?
これは可能ですか?
400 万人のユーザーの ID を格納する「IEnumerable users」を持つ C# プログラムを開発しています。別のメソッドでいくつかの操作を実行するには、毎回 IEnumerable をループしてバッチ 1000 の ID を抽出する必要があります。
IEnumerable の先頭から一度に 1000 個の ID を抽出し、何か別のことを行ってから、次の 1000 個のバッチを取得するにはどうすればよいですか?
これは可能ですか?
MoreLINQのバッチ演算子(NuGetから入手可能)を使用できます。
foreach(IEnumerable<User> batch in users.Batch(1000))
// use batch
ライブラリの単純な使用がオプションでない場合は、実装を再利用できます。
public static IEnumerable<IEnumerable<T>> Batch<T>(
this IEnumerable<T> source, int size)
{
T[] bucket = null;
var count = 0;
foreach (var item in source)
{
if (bucket == null)
bucket = new T[size];
bucket[count++] = item;
if (count != size)
continue;
yield return bucket.Select(x => x);
bucket = null;
count = 0;
}
// Return the last bucket with all remaining elements
if (bucket != null && count > 0)
{
Array.Resize(ref bucket, count);
yield return bucket.Select(x => x);
}
}
ところで、パフォーマンスのために、を呼び出さずにバケットを返すことができますSelect(x => x)
。Selectは配列用に最適化されていますが、セレクターデリゲートは引き続き各アイテムで呼び出されます。だから、あなたの場合は使用する方が良いです
yield return bucket;
オブジェクトのSkipメソッドとTakeメソッドを使用する必要があるようです。例:
users.Skip(1000).Take(1000)
これにより、最初の1000がスキップされ、次の1000が取得されます。呼び出しごとにスキップされる量を増やす必要があります。
Skipのパラメーターで整数変数を使用でき、スキップされる量を調整できます。その後、メソッドで呼び出すことができます。
public IEnumerable<user> GetBatch(int pageNumber)
{
return users.Skip(pageNumber * 1000).Take(1000);
}
これを行う最も簡単な方法は、おそらくGroupBy
LINQのメソッドを使用することです。
var batches = myEnumerable
.Select((x, i) => new { x, i })
.GroupBy(p => (p.i / 1000), (p, i) => p.x);
ただし、より高度なソリューションについては、これを行うための独自の拡張メソッドを作成する方法について、このブログ投稿を参照してください。後世のためにここに複製:
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> collection, int batchSize)
{
List<T> nextbatch = new List<T>(batchSize);
foreach (T item in collection)
{
nextbatch.Add(item);
if (nextbatch.Count == batchSize)
{
yield return nextbatch;
nextbatch = new List<T>();
// or nextbatch.Clear(); but see Servy's comment below
}
}
if (nextbatch.Count > 0)
yield return nextbatch;
}
これを使ってみてください:
public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
this IEnumerable<TSource> source,
int batchSize)
{
var batch = new List<TSource>();
foreach (var item in source)
{
batch.Add(item);
if (batch.Count == batchSize)
{
yield return batch;
batch = new List<TSource>();
}
}
if (batch.Any()) yield return batch;
}
上記の関数を使用するには:
foreach (var list in Users.Batch(1000))
{
}
これは、Take andSkipEnumerable拡張メソッドを使用して実現できます。使用状況チェックアウトlinq101の詳細については
次のようなものが機能します。
List<MyClass> batch = new List<MyClass>();
foreach (MyClass item in items)
{
batch.Add(item);
if (batch.Count == 1000)
{
// Perform operation on batch
batch.Clear();
}
}
// Process last batch
if (batch.Any())
{
// Perform operation on batch
}
そして、これを次のようにジェネリック メソッドに一般化できます。
static void PerformBatchedOperation<T>(IEnumerable<T> items,
Action<IEnumerable<T>> operation,
int batchSize)
{
List<T> batch = new List<T>();
foreach (T item in items)
{
batch.Add(item);
if (batch.Count == batchSize)
{
operation(batch);
batch.Clear();
}
}
// Process last batch
if (batch.Any())
{
operation(batch);
}
}
使用できますTake operator linq
リンク: http://msdn.microsoft.com/fr-fr/library/vstudio/bb503062.aspx
列挙子がバッチの途中でブロックされる可能性があるストリーミング コンテキストでは、値がまだ生成されていない (yield) という理由だけで、タイムアウト メソッドを使用すると、特定の時間後に最後のバッチが生成されるようになります。たとえば、MongoDB でカーソルをテーリングするためにこれを使用しました。列挙は別のスレッドで行う必要があるため、少し複雑です。
public static IEnumerable<List<T>> TimedBatch<T>(this IEnumerable<T> collection, double timeoutMilliseconds, long maxItems)
{
object _lock = new object();
List<T> batch = new List<T>();
AutoResetEvent yieldEventTriggered = new AutoResetEvent(false);
AutoResetEvent yieldEventFinished = new AutoResetEvent(false);
bool yieldEventTriggering = false;
var task = Task.Run(delegate
{
foreach (T item in collection)
{
lock (_lock)
{
batch.Add(item);
if (batch.Count == maxItems)
{
yieldEventTriggering = true;
yieldEventTriggered.Set();
}
}
if (yieldEventTriggering)
{
yieldEventFinished.WaitOne(); //wait for the yield to finish, and batch to be cleaned
yieldEventTriggering = false;
}
}
});
while (!task.IsCompleted)
{
//Wait for the event to be triggered, or the timeout to finish
yieldEventTriggered.WaitOne(TimeSpan.FromMilliseconds(timeoutMilliseconds));
lock (_lock)
{
if (batch.Count > 0) //yield return only if the batch accumulated something
{
yield return batch;
batch.Clear();
yieldEventFinished.Set();
}
}
}
task.Wait();
}