Дополнение после очень полезного комментария mhand в конце
Оригинальный ответ
Хотя большинство решений могут работать, я думаю, что они не очень эффективны. Предположим, если вы хотите только первые несколько элементов из первых нескольких кусков. Тогда вы не захотите перебирать все (zillion) элементы в вашей последовательности.
Следующее будет, в крайнем случае, перечисляться дважды: один раз для дубля и один раз для пропуска. Он не будет перечислять больше элементов, чем вы будете использовать:
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 кусков. Из каждого перечисленного фрагмента вы будете перечислять только первые М элементов.
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 элементов первого блока, то возврат доходности будет выполнен ровно 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
.
Итак, для каждого чанка мы видим, что сделано следующее:
- Any (): GetEnumerator; 1 MoveNext (); Утилизировать перечислитель;
Take ():
- ничего, если содержимое чанка не перечислено.
Если содержимое перечисляется: GetEnumerator (), один MoveNext и один Current для каждого перечисляемого элемента, Dispose enumerator;
Skip (): для каждого перечисляемого чанка (НЕ для содержимого чанка): GetEnumerator (), MoveNext () chunkSize times, без Current! Распорядиться перечислителем
Если вы посмотрите на то, что происходит с перечислителем, вы увидите, что есть много вызовов MoveNext (), и только вызовы Current
для тех элементов TSource, к которым вы фактически решаете обратиться.
Если вы возьмете N Chunks размером chunkSize, то вызовите MoveNext ()
- N раз для любого ()
- пока нет времени для Take, пока вы не перечислите чанки
- N раз chunkSize для 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 возвращает объект List, DbContext запоминает, что данные уже получены.
От вашего репозитория зависит, целесообразно ли вызывать AsEnumerable () или ToLists (), прежде чем вы начнете делить элементы на куски