Понимание поведения пользовательского фрагмента Linq и IEnumerable<IEnumerable<T>>

Я попытался реализовать пользовательскую функцию 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(), и там что-то идет не так, но я не знаю, что именно.

Внешний цикл не должен вызывать MoveNext().

ScottyD0nt 31.10.2022 18:30

Проблема в том, что вы не пропустили элементы в своем цикле.

Rivo R. 31.10.2022 18:40

Примечание: то, что вы пытаетесь сделать (иметь два или более итераторов, активно указывающих на разные позиции в исходной последовательности), просто невозможно. Поэтому все, что вы пытаетесь сделать, так или иначе потерпит неудачу. Вы должны нелениво перебирать внутренние последовательности.

Alexei Levenkov 31.10.2022 18:51
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
3
56
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Попробуйте так:

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 или другие операции ввода-вывода для получения данных), и, кроме того, она может не создавать одинаковое количество элементов при каждой итерации, поэтому упомянутое множественное перечисление влияет как на производительность, так и на правильность .

Servy 31.10.2022 18:47

Вы создали интератор в итераторе, но только внешний итератор выполняется в 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 не работает (поскольку они, вероятно, не понимают ленивую оценку) как а также объяснить, что два "указателя" на одно и то же перечисляемое невозможны (у меня нет хорошего объяснения, иначе я бы написал сам)...

Alexei Levenkov 31.10.2022 18:56

Я знаю о вашем подходе и уже реализовал его. Но меня интересовало поведение IEnumerable и yield. Итак, теперь я понял, спасибо большое

Kosmonik 31.10.2022 19:38
Ответ принят как подходящий

У меня есть предположение, что это происходит потому, что внутренний 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 всегда потребляются.

Другие вопросы по теме