誰かがlinqで特定のサイズのバッチを作成する方法を提案できますか?
理想的には、構成可能な量のチャンクで操作を実行できるようにしたいと考えています。
コードを書く必要はありません。ソース シーケンスをサイズの大きなバケットにバッチ処理するMoreLINQバッチ メソッドを使用します (MoreLINQ は、インストール可能な NuGet パッケージとして利用できます)。
int size = 10;
var batches = sequence.Batch(size);
次のように実装されます。
public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
this IEnumerable<TSource> source, int size)
{
TSource[] bucket = null;
var count = 0;
foreach (var item in source)
{
if (bucket == null)
bucket = new TSource[size];
bucket[count++] = item;
if (count != size)
continue;
yield return bucket;
bucket = null;
count = 0;
}
if (bucket != null && count > 0)
yield return bucket.Take(count).ToArray();
}
public static class MyExtensions
{
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> items,
int maxItems)
{
return items.Select((item, inx) => new { item, inx })
.GroupBy(x => x.inx / maxItems)
.Select(g => g.Select(x => x.item));
}
}
使用法は次のようになります。
List<int> list = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
foreach(var batch in list.Batch(3))
{
Console.WriteLine(String.Join(",",batch));
}
出力:
0,1,2
3,4,5
6,7,8
9
上記のすべては、大きなバッチまたはメモリ容量が少ない場合にひどく実行されます。パイプラインする独自のものを作成する必要がありました(どこにもアイテムが蓄積されていないことに注意してください):
public static class BatchLinq {
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size) {
if (size <= 0)
throw new ArgumentOutOfRangeException("size", "Must be greater than zero.");
using (IEnumerator<T> enumerator = source.GetEnumerator())
while (enumerator.MoveNext())
yield return TakeIEnumerator(enumerator, size);
}
private static IEnumerable<T> TakeIEnumerator<T>(IEnumerator<T> source, int size) {
int i = 0;
do
yield return source.Current;
while (++i < size && source.MoveNext());
}
}
編集:このアプローチの既知の問題は、次のバッチに移動する前に、各バッチを列挙し、完全に列挙する必要があることです。たとえば、これは機能しません:
//Select first item of every 100 items
Batch(list, 100).Select(b => b.First())
これは、Nick Whaley (リンク) と infogulch (リンク) の遅延Batch
実装の改善を試みたものです。こいつは厳しい。バッチを正しい順序で列挙するか、例外を取得します。
public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
this IEnumerable<TSource> source, int size)
{
if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
using (var enumerator = source.GetEnumerator())
{
int i = 0;
while (enumerator.MoveNext())
{
if (i % size != 0) throw new InvalidOperationException(
"The enumeration is out of order.");
i++;
yield return GetBatch();
}
IEnumerable<TSource> GetBatch()
{
while (true)
{
yield return enumerator.Current;
if (i % size == 0 || !enumerator.MoveNext()) break;
i++;
}
}
}
}
そして、これは type のソースの遅延Batch
実装ですIList<T>
。これは、列挙に制限を課しません。バッチは、任意の順序で、複数回、部分的に列挙できます。ただし、列挙中にコレクションを変更しないという制限は引き続き適用されます。enumerator.MoveNext()
これは、チャンクまたは要素を生成する前にダミーの呼び出しを行うことによって実現されます。欠点は、列挙がいつ終了するかわからないため、列挙子が未処理のままになることです。
public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
this IList<TSource> source, int size)
{
if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
var enumerator = source.GetEnumerator();
for (int i = 0; i < source.Count; i += size)
{
enumerator.MoveNext();
yield return GetChunk(i, Math.Min(i + size, source.Count));
}
IEnumerable<TSource> GetChunk(int from, int toExclusive)
{
for (int j = from; j < toExclusive; j++)
{
enumerator.MoveNext();
yield return source[j];
}
}
}
私はこれに非常に遅く参加しましたが、もっと興味深いものを見つけました。
したがって、ここSkip
で使用Take
してパフォーマンスを向上させることができます。
public static class MyExtensions
{
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> items, int maxItems)
{
return items.Select((item, index) => new { item, index })
.GroupBy(x => x.index / maxItems)
.Select(g => g.Select(x => x.item));
}
public static IEnumerable<T> Batch2<T>(this IEnumerable<T> items, int skip, int take)
{
return items.Skip(skip).Take(take);
}
}
次に、100000 レコードでチェックしました。次の場合、ループのみに時間がかかりますBatch
コンソール アプリケーションのコード。
static void Main(string[] args)
{
List<string> Ids = GetData("First");
List<string> Ids2 = GetData("tsriF");
Stopwatch FirstWatch = new Stopwatch();
FirstWatch.Start();
foreach (var batch in Ids2.Batch(5000))
{
// Console.WriteLine("Batch Ouput:= " + string.Join(",", batch));
}
FirstWatch.Stop();
Console.WriteLine("Done Processing time taken:= "+ FirstWatch.Elapsed.ToString());
Stopwatch Second = new Stopwatch();
Second.Start();
int Length = Ids2.Count;
int StartIndex = 0;
int BatchSize = 5000;
while (Length > 0)
{
var SecBatch = Ids2.Batch2(StartIndex, BatchSize);
// Console.WriteLine("Second Batch Ouput:= " + string.Join(",", SecBatch));
Length = Length - BatchSize;
StartIndex += BatchSize;
}
Second.Stop();
Console.WriteLine("Done Processing time taken Second:= " + Second.Elapsed.ToString());
Console.ReadKey();
}
static List<string> GetData(string name)
{
List<string> Data = new List<string>();
for (int i = 0; i < 100000; i++)
{
Data.Add(string.Format("{0} {1}", name, i.ToString()));
}
return Data;
}
かかった時間はこんな感じ。
最初 - 00:00:00.0708、00:00:00.0660
秒 (Take and Skip One) - 00:00:00.0008、00:00:00.0008