Відповіді:
Вам не потрібно писати будь-який код. Використовуйте пакетний метод 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();
}
Batch(new int[] { 1, 2 }, 1000000)
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
GroupBy
починається перерахування, чи не потрібно повністю перераховувати його джерело? Це втрачає ледачу оцінку джерела, а отже, в деяких випадках і всю користь від видобутку!
Якщо ви почнете з sequence
визначеного як an IEnumerable<T>
, і ви знаєте, що його можна сміливо перераховувати кілька разів (наприклад, тому, що це масив або список), ви можете просто використовувати цю просту схему для обробки елементів в партіях:
while (sequence.Any())
{
var batch = sequence.Take(10);
sequence = sequence.Skip(10);
// do whatever you need to do with each batch here
}
Все вищесказане працює надзвичайно великими партіями або мало місця в пам'яті. Довелося написати моє власне, що буде прокладено (не помічайте ніде накопичення елементів):
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())
Це повністю ледача, низька накладні витрати, однофункціональна реалізація Batch, яка не накопичує. На основі (і виправляє проблеми в) Нік Уейл в розчині за допомогою EricRoller.
Ітерація надходить безпосередньо від основної IEnumerable, тому елементи повинні бути перераховані в суворому порядку і доступ до них не більше одного разу. Якщо деякі елементи не споживаються у внутрішньому циклі, вони відкидаються (і спроба знову отримати доступ до них через збережений ітератор буде кинутий InvalidOperationException: Enumeration already finished.
).
Ви можете протестувати повний зразок у .NET Fiddle .
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 (var enumerator = source.GetEnumerator())
while (enumerator.MoveNext())
{
int i = 0;
// Batch is a local function closing over `i` and `enumerator` that
// executes the inner batch enumeration
IEnumerable<T> Batch()
{
do yield return enumerator.Current;
while (++i < size && enumerator.MoveNext());
}
yield return Batch();
while (++i < size && enumerator.MoveNext()); // discard skipped items
}
}
}
done
, просто зателефонувавши e.Count()
після yield return e
. Вам потрібно буде переставити цикл у BatchInner, щоб не викликати невизначене поведінку, source.Current
якщо i >= size
. Це позбавить від необхідності виділяти нову BatchInner
для кожної партії.
i
тому це не обов'язково більш ефективно, ніж визначення окремого класу, але я думаю, що це трохи чистіше.
Цікаво, чому ніхто ніколи не розміщував стару школу для вирішення проблем. Ось один:
List<int> source = Enumerable.Range(1,23).ToList();
int batchsize = 10;
for (int i = 0; i < source.Count; i+= batchsize)
{
var batch = source.Skip(i).Take(batchsize);
}
Ця простота можлива, оскільки метод Take:
... перераховує
source
та отримує елементи, покиcount
елементи не виведені абоsource
не містять більше елементів. Якщоcount
кількість елементів у них перевищуєsource
, всі елементиsource
повертаються
Відмова:
Використання Пропустіть і Взяти всередині циклу означає, що перераховуються перераховуються кілька разів. Це небезпечно, якщо нумероване число відкладено. Це може спричинити багаторазове виконання запиту до бази даних, веб-запиту або зчитування файлу. Цей приклад явно стосується використання списку, який не відкладений, тому це менше проблем. Це все ще повільне рішення, оскільки пропуск перераховує колекцію кожного разу, коли вона викликається.
Це також можна вирішити за допомогою GetRange
методу, але для вилучення можливої групи спокою потрібен додатковий розрахунок:
for (int i = 0; i < source.Count; i += batchsize)
{
int remaining = source.Count - i;
var batch = remaining > batchsize ? source.GetRange(i, batchsize) : source.GetRange(i, remaining);
}
Ось третій спосіб впоратися з цим, який працює з 2-х петель. Це гарантує, що колекція перераховується лише 1 раз !:
int batchsize = 10;
List<int> batch = new List<int>(batchsize);
for (int i = 0; i < source.Count; i += batchsize)
{
// calculated the remaining items to avoid an OutOfRangeException
batchsize = source.Count - i > batchsize ? batchsize : source.Count - i;
for (int j = i; j < i + batchsize; j++)
{
batch.Add(source[j]);
}
batch.Clear();
}
Skip
і Take
всередині циклу означає, що перераховуються перераховуються кілька разів. Це небезпечно, якщо нумероване число відкладено. Це може спричинити багаторазове виконання запиту до бази даних, веб-запиту або зчитування файлу. У вашому прикладі ви маєте значення, List
яке не відкладається, тому це менше проблем.
Той самий підхід, що і MoreLINQ, але використовуючи List замість Array. Я не робив бенчмаркінгу, але читабельність важлива для деяких людей:
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
{
List<T> batch = new List<T>();
foreach (var item in source)
{
batch.Add(item);
if (batch.Count >= size)
{
yield return batch;
batch.Clear();
}
}
if (batch.Count > 0)
{
yield return batch;
}
}
size
параметр вашому, new List
щоб оптимізувати його розмір.
batch.Clear();
наbatch = new List<T>();
Ось спроба вдосконалення лінивих реалізацій Ніка Уолі ( посилання ) та 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++;
}
}
}
}
І ось лінива 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
Другий (Візьміть і пропустіть один) - 00: 00: 00.0008, 00: 00: 00.0008
GroupBy
повністю перераховується до того, як виробляє один ряд. Це не гарний спосіб зробити дозування.
foreach (var batch in Ids2.Batch(5000))
щоб var gourpBatch = Ids2.Batch(5000)
і перевірити синхронізовані результати. або додати до списку var SecBatch = Ids2.Batch2(StartIndex, BatchSize);
я був би зацікавлений, якщо ваші результати зміни часу змінитимуться.
Отож, у функціональній шапці це виглядає тривіально .... але у C # є деякі суттєві недоліки.
ви, мабуть, розглядаєте це як розгортання IEnumerable (google це, і ви, ймовірно, опинитесь в деяких документах Haskell, але, можливо, є деякі F # речі, використовуючи розгортання, якщо ви знаєте F #, прижмуріться до документів Haskell, і це зробить сенс).
Розгортання пов'язане зі складанням ("сукупністю"), за винятком того, а не з повторенням через вхідний IEnumerable, воно повторюється через структури вихідних даних (подібний взаємозв'язок між IEnumerable та IObservable, адже я думаю, що IObservable реалізує "розгорнутий" під назвою генерувати. ..)
у будь-якому випадку спочатку вам потрібен метод розгортання, я думаю, що це працює (на жаль, він врешті-решт підіб'є стек для великих "списків" ... ви можете сміливо писати це у F #, використовуючи дохід! а не concat);
static IEnumerable<T> Unfold<T, U>(Func<U, IEnumerable<Tuple<U, T>>> f, U seed)
{
var maybeNewSeedAndElement = f(seed);
return maybeNewSeedAndElement.SelectMany(x => new[] { x.Item2 }.Concat(Unfold(f, x.Item1)));
}
це трохи тупо, тому що C # не реалізує деякі речі, які функціональні мови приймають як належне ... але в основному це бере насіння, а потім генерує "Можливо" відповідь наступного елемента в IEnumerable та наступного насіння (можливо не існує в C #, тому ми використовували IEnumerable, щоб підробити це), і поєднує решту відповідей (я не можу порушити складність "O (n?)" цього).
Після того, як ви це зробили тоді;
static IEnumerable<IEnumerable<T>> Batch<T>(IEnumerable<T> xs, int n)
{
return Unfold(ys =>
{
var head = ys.Take(n);
var tail = ys.Skip(n);
return head.Take(1).Select(_ => Tuple.Create(tail, head));
},
xs);
}
все виглядає досить чисто ... ви приймаєте елементи "n" як "наступний" елемент IEnumerable, а "хвіст" - це решта не обробленого списку.
якщо в голові нічого немає ... ви закінчилися ... ви повертаєте "Нічого" (але підроблений як порожній IEnumerable>) ... ще повертаєте головний елемент і хвіст для обробки.
ви, мабуть, можете це зробити за допомогою IObservable, там, мабуть, існує такий тип "Batch", і ви, ймовірно, можете це використовувати.
Якщо ризик стека переповнює занепокоєння (це, мабуть, повинно бути), тоді вам слід реалізувати у F # (а можливо, вже існує і якась бібліотека F # (FSharpX?)).
(Я робив лише деякі рудиментарні тести на це, тому там можуть бути дивні помилки).
Я написав власну IEnumerable реалізацію, яка працює без linq і гарантує єдине перерахування даних. Він також виконує все це, не вимагаючи резервних списків або масивів, які викликають вибухи пам'яті у великих наборах даних.
Ось кілька основних тестів:
[Fact]
public void ShouldPartition()
{
var ints = new List<int> {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
var data = ints.PartitionByMaxGroupSize(3);
data.Count().Should().Be(4);
data.Skip(0).First().Count().Should().Be(3);
data.Skip(0).First().ToList()[0].Should().Be(0);
data.Skip(0).First().ToList()[1].Should().Be(1);
data.Skip(0).First().ToList()[2].Should().Be(2);
data.Skip(1).First().Count().Should().Be(3);
data.Skip(1).First().ToList()[0].Should().Be(3);
data.Skip(1).First().ToList()[1].Should().Be(4);
data.Skip(1).First().ToList()[2].Should().Be(5);
data.Skip(2).First().Count().Should().Be(3);
data.Skip(2).First().ToList()[0].Should().Be(6);
data.Skip(2).First().ToList()[1].Should().Be(7);
data.Skip(2).First().ToList()[2].Should().Be(8);
data.Skip(3).First().Count().Should().Be(1);
data.Skip(3).First().ToList()[0].Should().Be(9);
}
Метод розширення для розділення даних.
/// <summary>
/// A set of extension methods for <see cref="IEnumerable{T}"/>.
/// </summary>
public static class EnumerableExtender
{
/// <summary>
/// Splits an enumerable into chucks, by a maximum group size.
/// </summary>
/// <param name="source">The source to split</param>
/// <param name="maxSize">The maximum number of items per group.</param>
/// <typeparam name="T">The type of item to split</typeparam>
/// <returns>A list of lists of the original items.</returns>
public static IEnumerable<IEnumerable<T>> PartitionByMaxGroupSize<T>(this IEnumerable<T> source, int maxSize)
{
return new SplittingEnumerable<T>(source, maxSize);
}
}
Це клас реалізації
using System.Collections;
using System.Collections.Generic;
internal class SplittingEnumerable<T> : IEnumerable<IEnumerable<T>>
{
private readonly IEnumerable<T> backing;
private readonly int maxSize;
private bool hasCurrent;
private T lastItem;
public SplittingEnumerable(IEnumerable<T> backing, int maxSize)
{
this.backing = backing;
this.maxSize = maxSize;
}
public IEnumerator<IEnumerable<T>> GetEnumerator()
{
return new Enumerator(this, this.backing.GetEnumerator());
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
private class Enumerator : IEnumerator<IEnumerable<T>>
{
private readonly SplittingEnumerable<T> parent;
private readonly IEnumerator<T> backingEnumerator;
private NextEnumerable current;
public Enumerator(SplittingEnumerable<T> parent, IEnumerator<T> backingEnumerator)
{
this.parent = parent;
this.backingEnumerator = backingEnumerator;
this.parent.hasCurrent = this.backingEnumerator.MoveNext();
if (this.parent.hasCurrent)
{
this.parent.lastItem = this.backingEnumerator.Current;
}
}
public bool MoveNext()
{
if (this.current == null)
{
this.current = new NextEnumerable(this.parent, this.backingEnumerator);
return true;
}
else
{
if (!this.current.IsComplete)
{
using (var enumerator = this.current.GetEnumerator())
{
while (enumerator.MoveNext())
{
}
}
}
}
if (!this.parent.hasCurrent)
{
return false;
}
this.current = new NextEnumerable(this.parent, this.backingEnumerator);
return true;
}
public void Reset()
{
throw new System.NotImplementedException();
}
public IEnumerable<T> Current
{
get { return this.current; }
}
object IEnumerator.Current
{
get { return this.Current; }
}
public void Dispose()
{
}
}
private class NextEnumerable : IEnumerable<T>
{
private readonly SplittingEnumerable<T> splitter;
private readonly IEnumerator<T> backingEnumerator;
private int currentSize;
public NextEnumerable(SplittingEnumerable<T> splitter, IEnumerator<T> backingEnumerator)
{
this.splitter = splitter;
this.backingEnumerator = backingEnumerator;
}
public bool IsComplete { get; private set; }
public IEnumerator<T> GetEnumerator()
{
return new NextEnumerator(this.splitter, this, this.backingEnumerator);
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
private class NextEnumerator : IEnumerator<T>
{
private readonly SplittingEnumerable<T> splitter;
private readonly NextEnumerable parent;
private readonly IEnumerator<T> enumerator;
private T currentItem;
public NextEnumerator(SplittingEnumerable<T> splitter, NextEnumerable parent, IEnumerator<T> enumerator)
{
this.splitter = splitter;
this.parent = parent;
this.enumerator = enumerator;
}
public bool MoveNext()
{
this.parent.currentSize += 1;
this.currentItem = this.splitter.lastItem;
var hasCcurent = this.splitter.hasCurrent;
this.parent.IsComplete = this.parent.currentSize > this.splitter.maxSize;
if (this.parent.IsComplete)
{
return false;
}
if (hasCcurent)
{
var result = this.enumerator.MoveNext();
this.splitter.lastItem = this.enumerator.Current;
this.splitter.hasCurrent = result;
}
return hasCcurent;
}
public void Reset()
{
throw new System.NotImplementedException();
}
public T Current
{
get { return this.currentItem; }
}
object IEnumerator.Current
{
get { return this.Current; }
}
public void Dispose()
{
}
}
}
}
Я знаю, що всі використовували складні системи для цієї роботи, і я дійсно не розумію, чому. Зняти і пропустити дозволить усі ці операції, що використовують загальний вибір з Func<TSource,Int32,TResult>
функцією перетворення. Подібно до:
public IEnumerable<IEnumerable<T>> Buffer<T>(IEnumerable<T> source, int size)=>
source.Select((item, index) => source.Skip(size * index).Take(size)).TakeWhile(bucket => bucket.Any());
source
будуть повторюватися дуже часто.
Enumerable.Range(0, 1).SelectMany(_ => Enumerable.Range(0, new Random().Next()))
.
Ще одна реалізація одного рядка. Це працює навіть з порожнім списком, в цьому випадку ви отримуєте колекцію партій нульового розміру.
var aList = Enumerable.Range(1, 100).ToList(); //a given list
var size = 9; //the wanted batch size
//number of batches are: (aList.Count() + size - 1) / size;
var batches = Enumerable.Range(0, (aList.Count() + size - 1) / size).Select(i => aList.GetRange( i * size, Math.Min(size, aList.Count() - i * size)));
Assert.True(batches.Count() == 12);
Assert.AreEqual(batches.ToList().ElementAt(0), new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
Assert.AreEqual(batches.ToList().ElementAt(1), new List<int>() { 10, 11, 12, 13, 14, 15, 16, 17, 18 });
Assert.AreEqual(batches.ToList().ElementAt(11), new List<int>() { 100 });
Інший спосіб - використання оператора буфера Rx
//using System.Linq;
//using System.Reactive.Linq;
//using System.Reactive.Threading.Tasks;
var observableBatches = anAnumerable.ToObservable().Buffer(size);
var batches = aList.ToObservable().Buffer(size).ToList().ToTask().GetAwaiter().GetResult();
GetAwaiter().GetResult()
. Це запах коду для синхронного коду, який насильно викликає асинхронний код.
static IEnumerable<IEnumerable<T>> TakeBatch<T>(IEnumerable<T> ts,int batchSize)
{
return from @group in ts.Select((x, i) => new { x, i }).ToLookup(xi => xi.i / batchSize)
select @group.Select(xi => xi.x);
}