Почему этот метод приводит к бесконечному циклу?

Один из моих коллег пришел ко мне с вопросом об этом методе, который приводит к бесконечному циклу. Фактический код слишком запутан, чтобы публиковать здесь, но по существу проблема сводится к следующему:

private IEnumerable<int> GoNuts(IEnumerable<int> items)
{
    items = items.Select(item => items.First(i => i == item));
    return items;
}

Это должно (вы думаете) просто быть очень неэффективным способом создания копии списка. Я вызвал его с помощью:

var foo = GoNuts(new[]{1,2,3,4,5,6});

В результате получается бесконечный цикл. Странно.

Я думаю, что изменение параметра, стилистически плохое, поэтому я немного изменил код:

var foo = items.Select(item => items.First(i => i == item));
return foo;

Это сработало. То есть, программа завершена; не исключение.

Другие эксперименты показали, что это тоже работает:

items = items.Select(item => items.First(i => i == item)).ToList();
return items;

Как и простой

return items.Select(item => .....);

Любопытный.

Понятно, что проблема связана с переназначением параметра, но только если оценка отложена за пределами этого утверждения. Если я добавлю ToList(), он будет работать.

У меня есть общая, неопределенная идея о том, что происходит не так. Похоже, что Select выполняет итерацию по собственному результату. Это немного странно само по себе, потому что обычно IEnumerable будет бросать, если изменится его итерация коллекции.

То, что я не понимаю, потому что я не очень хорошо знаком с внутренними функциями того, как работает этот материал, объясняется тем, почему повторное назначение параметра вызывает этот бесконечный цикл.

Есть ли у кого-то больше знаний о внутренностях, которые хотели бы объяснить, почему здесь происходит бесконечный цикл?

+64
источник поделиться
3 ответа

Ключ к ответу на это - отсроченное исполнение. Когда вы это сделаете

items = items.Select(item => items.First(i => i == item));

вы не перебираете массив items, переданный в метод. Вместо этого вы назначаете ему новый IEnumerable<int>, который ссылается на себя и запускает итерацию только тогда, когда вызывающий абонент начинает перечислять результаты.

Вот почему все ваши другие исправления затронули проблему: все, что вам нужно сделать, это прекратить кормить IEnumerable<int> назад:

  • Использование var foo разбивает самооценку с помощью другой переменной,
  • Использование return items.Select... разбивает самооценку, вообще не используя промежуточные переменные,
  • Использование ToList() ломает самосогласование, избегая отложенного выполнения: к моменту items переназначается старая items, итерация завершена, поэтому вы получите обычную память List<int>.

Но если он питается сам собой, как он вообще получает что-либо?

Правильно, ничего не получается! В тот момент, когда вы пытаетесь выполнить итерацию items и запрашиваете его для первого элемента, отложенная последовательность запрашивает последовательность, поданную ему для первого элемента для обработки, что означает, что последовательность запрашивает себя для первого элемента для обработки. На данный момент он черепахи полностью опущен, потому что для того, чтобы вернуть первый элемент для обработки последовательности, сначала нужно получить первый элемент для обработки из себя.

+63
источник

Похоже, что Select выполняет итерацию над собственным выходом

Вы правы. Вы возвращаете запрос, который выполняет итерацию по себе.

Ключ в том, что вы ссылаетесь на items в лямбда. Ссылка items не разрешена ( "закрыта" ) до тех пор, пока запрос не повторится, и в этот момент items теперь ссылается на запрос вместо исходной коллекции. Вот где происходит самооценка.

Изобразите колоду карт со знаком перед ней с надписью items. Теперь изобразите человека, стоящего рядом с колодой карт, назначение которых состоит в повторении коллекции под названием items. Но тогда вы перемещаете знак с колоды на человека. Когда вы спрашиваете человека за первый "предмет" - он ищет коллекцию с надписью "предметы" - которая теперь его! Поэтому он спрашивает себя о первом элементе, в котором происходит круговая ссылка.

Когда вы назначаете результат новой переменной, вы затем получаете запрос, который выполняет итерацию по другой коллекции, и поэтому не приводит к бесконечному циклу.

Когда вы вызываете ToList, вы убираете запрос в новую коллекцию, а также не получаете бесконечный цикл.

Другие вещи, которые нарушили бы круглую ссылку:

  • Увлажняющие элементы в лямбда, вызывая ToList
  • Назначение items другой переменной и ссылка на нее в лямбда.
+20
источник

После изучения двух ответов, которые даны и немного соскучились, я придумал небольшую программу, которая лучше иллюстрирует проблему.

    private int GetFirst(IEnumerable<int> items, int foo)
    {
        Console.WriteLine("GetFirst {0}", foo);
        var rslt = items.First(i => i == foo);
        Console.WriteLine("GetFirst returns {0}", rslt);
        return rslt;
    }

    private IEnumerable<int> GoNuts(IEnumerable<int> items)
    {
        items = items.Select(item =>
        {
            Console.WriteLine("Select item = {0}", item);
            return GetFirst(items, item);
        });
        return items;
    }

Если вы вызываете это с помощью:

var newList = GoNuts(new[]{1, 2, 3, 4, 5, 6});

Вы будете получать этот вывод несколько раз, пока не получите StackOverflowException.

Select item = 1
GetFirst 1
Select item = 1
GetFirst 1
Select item = 1
GetFirst 1
...

Что это показывает, именно то, что dasblinkenlight ясно дал в его обновленном ответе: запрос переходит в бесконечный цикл, пытаясь получить первый элемент.

Давайте напишем GoNuts несколько иначе:

    private IEnumerable<int> GoNuts(IEnumerable<int> items)
    {
        var originalItems = items;
        items = items.Select(item =>
        {
            Console.WriteLine("Select item = {0}", item);
            return GetFirst(originalItems, item);
        });
        return items;
    }

Если вы запустите это, оно будет успешным. Зачем? Поскольку в этом случае ясно, что вызов GetFirst передает ссылку на исходные элементы, которые были переданы методу. В первом случае GetFirst передает ссылку на новую коллекцию items, которая еще не реализована. В свою очередь, GetFirst говорит: "Эй, мне нужно перечислить эту коллекцию". И таким образом начинается первый рекурсивный вызов, который в конечном итоге приводит к StackOverflowException.

Интересно, что я был прав и не прав, когда сказал, что он потребляет собственный результат. Select потребляет исходный вход, как и следовало ожидать. First пытается использовать выход.

Здесь много уроков. Для меня наиболее важным является "не изменять значение входных параметров".

Благодаря dasblinkenlight, D Stanley и Lucas Trzesniewski за их помощь.

+5
источник

Посмотрите другие вопросы по меткам или Задайте вопрос