Доповнення після дуже корисного коментаря 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())
{
...
}
будь-який отримає перелік, зробить 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;
}
}
Це не робить багато, поки ви не почнете перераховувати над отриманим фрагментом. Якщо ви отримаєте кілька фрагментів, але вирішите не перелічувати перший фрагмент, передбачення не виконується, як вам покаже ваш налагоджувач.
Якщо ви вирішили взяти перші елементи M першого шару, то повернення виходу виконується рівно M разів. Це означає:
- дістати нумератор
- виклик MoveNext () та Поточний 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;
}
}
}
Як бачите, SkipIterator
дзвінки MoveNext()
один раз для кожного елемента в куску. Це не дзвонить Current
.
Отже, за Чанком ми бачимо, що робиться наступне:
- Будь-яке (): GetEnumerator; 1 MoveNext (); Розпоряджається перерахувачем;
Приймати():
- нічого, якщо зміст шматка не перелічено.
Якщо вміст перераховано: GetEnumerator (), один MoveNext та один Поточний на перерахований елемент, розпорядження перерахуйте;
Пропустити (): для кожного перерахованого фрагмента (НЕ вміст шматка): GetEnumerator (), MoveNext () фрагментРазмер часу, без поточного! Утилізуйте перелік
Якщо ви подивитеся на те, що відбувається з перерахувачем, ви побачите, що до MoveNext () багато дзвінків, і лише дзвінки до Current
пунктів TSource, до яких ви вирішили отримати доступ.
Якщо ви приймаєте N Chunks розміру chunkSize, тоді дзвінки на MoveNext ()
- N разів для будь-якого ()
- ще немає часу для Take, доки ви не перерахуєте шматки
- N разів фрагментРозмір для 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 повертає об'єкт, що представляє собою Список, DbContext пам'ятає, що дані вже отримані.
Від вашого сховища залежить, чи розумно викликати AsEnumerable () або ToLists () перед тим, як почати ділити елементи в Chunks