Я попытался реализовать пользовательскую функцию Linq Chunk и нашел этот пример кода
This function should separate IEnumerable into IEnumerable of concrete size
public static class EnumerableExtentions
{
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
{
using (var enumerator = source.GetEnumerator())
{
while (enumerator.MoveNext())
{
int i = 0;
IEnumerable<T> Batch()
{
do yield return enumerator.Current;
while (++i < size && enumerator.MoveNext());
}
yield return Batch();
}
}
}
}
Итак, у меня есть вопрос. Почему, когда я пытаюсь выполнить какую-либо операцию Linq над результатом, они неверны? Например:
IEnumerable<int> list = Enumerable.Range(0, 10);
Console.WriteLine(list.Batch(2).Count()); // 10 instead of 5
У меня есть предположение, что это происходит из-за того, что внутренний IEnumerable Batch() срабатывает только при вызове Count(), и там что-то идет не так, но я не знаю, что именно.
Проблема в том, что вы не пропустили элементы в своем цикле.
Примечание: то, что вы пытаетесь сделать (иметь два или более итераторов, активно указывающих на разные позиции в исходной последовательности), просто невозможно. Поэтому все, что вы пытаетесь сделать, так или иначе потерпит неудачу. Вы должны нелениво перебирать внутренние последовательности.





Попробуйте так:
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> arr, int size)
{
for (var i = 0; i < arr.Count() / size + 1; i++)
{
yield return arr.Skip(i * size).Take(size);
}
}
1. Это никоим образом не отвечает на поставленный вопрос. 2) это ужасно неэффективная реализация этого метода, учитывая, сколько он повторяет последовательность с самого начала снова и снова 3) это повторяет исходный код много раз, что особенно проблематично, если последовательность имеет побочные эффекты или делает какие-либо дорогостоящие вычисления (наиболее частым из которых является то, что последовательность выполняет DB или другие операции ввода-вывода для получения данных), и, кроме того, она может не создавать одинаковое количество элементов при каждой итерации, поэтому упомянутое множественное перечисление влияет как на производительность, так и на правильность .
Вы создали интератор в итераторе, но только внешний итератор выполняется в Count(). Если вы хотите выполнить внутреннюю часть, вам нужно ее перечислить, например:
var batches = list.Batch(3);
foreach(var batch in batches) // the outer is executed
{
int count = batch.Count(); // the inner iterator is executed now
}
Ну, я бы предложил другой подход для метода Chunk, например:
public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
{
T[]? bucket = null;
var count = 0;
foreach (var item in source)
{
bucket ??= new T[size];
bucket[count++] = item;
if (count != size)
continue;
yield return bucket;
bucket = null;
count = 0;
}
if (count > 0)
{
Array.Resize(ref bucket, count);
yield return bucket;
}
}
Я думаю, что было бы лучше пропустить альтернативную реализацию в пользу ссылки на stackoverflow.com/questions/419019/… и потратить больше времени на объяснение того, почему код OP не работает (поскольку они, вероятно, не понимают ленивую оценку) как а также объяснить, что два "указателя" на одно и то же перечисляемое невозможны (у меня нет хорошего объяснения, иначе я бы написал сам)...
Я знаю о вашем подходе и уже реализовал его. Но меня интересовало поведение IEnumerable и yield. Итак, теперь я понял, спасибо большое
У меня есть предположение, что это происходит потому, что внутренний IEnumerable Batch() запускается только при вызове Count()
Это наоборот. Внутренний IEnumerable не расходуется, когда вы вызываете Count. Count потребляет только внешний IEnumerable, а именно этот:
while (enumerator.MoveNext())
{
int i = 0;
IEnumerable<T> Batch()
{
// the below is not executed by Count!
// do yield return enumerator.Current;
// while (++i < size && enumerator.MoveNext());
}
yield return Batch();
}
Так что Count просто переместит перечислитель в конец и подсчитает, сколько раз он его переместил, то есть 10.
Сравните это с тем, как автор этого, вероятно, намеревался использовать это:
foreach (var batch in someEnumerable.Batch(2)) {
foreach(var thing in batch) {
// ...
}
}
Я также использую внутренние IEnumerable, используя внутренний цикл, поэтому запускаю код внутри внутреннего Batch. Это дает текущий элемент, а затем также перемещает исходный перечислитель вперед. Он снова возвращает текущий элемент до того, как проверка ++i < size завершится ошибкой. Внешний цикл снова переместит перечислитель вперед для следующей итерации. И вот как вы создали «партию» из двух элементов.
Обратите внимание, что «перечислитель» (который произошел от someEnumerable) в предыдущем абзаце используется как внутренним, так и внешним IEnumerables. Использование внутреннего или внешнего IEnumerable приведет к перемещению счетчика, и только когда вы потребляете как внутренние, так и внешние IEnumerable очень специфическим образом, происходит последовательность действий, описанная в предыдущем абзаце, что приводит к получению пакетов.
В вашем случае вы можете использовать внутренние IEnumerable, позвонив ToList:
Console.WriteLine(list.Batch(2).Select(x => x.ToList()).Count()); // 5
Хотя совместное использование перечислителя здесь позволяет лениво использовать пакеты, это ограничивает клиентский код, чтобы использовать его только очень специфическими способами. В реализации Chunk в .NET 6 пакеты (чанки) быстро вычисляются как массивы:
public static IEnumerable<TSource[]> Chunk<TSource>(this IEnumerable<TSource> source, int size)
Вы можете сделать то же самое в своем Batch, позвонив ToArray() здесь:
yield return Batch().ToArray();
так что внутренние IEnumerable всегда потребляются.
Внешний цикл не должен вызывать MoveNext().