2015-08-13 3 views
64

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

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

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

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

+0

'items' переназначается до начала итерации. Вот почему вы получаете рекурсивный итератор. Этого не происходит, если вы вызываете 'ToList', поскольку назначение выполняется после итерации в этом случае. –

+0

Не отвечая, вы должны знать, что существует много способов клонирования списка. Вот несколько отличных ответов о том, как это сделать. http://stackoverflow.com/questions/222598/how-do-i-clone-a-generic-list-in-c – MiniRagnarok

+0

Спасибо, @MiniRagnarok. Я не пытался клонировать список, а использовал этот простой код, чтобы проиллюстрировать поведение бесконечной рекурсии более сложного метода. –

ответ

63

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

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 и запрашиваете ее для первого элемента, отложенная последовательность запрашивает последовательность, поданную ему для обработки первого элемента, что означает, что последовательность запрашивает у себя первый элемент для обработки. На данный момент это turtles all the way down, потому что для возврата первого элемента для обработки последовательность сначала должна получить первый элемент для обработки из себя.

+0

Но если он питается собой, как он вообще получает что-то? Это похоже на то, что запрос потребляет исходный вход, а затем его собственный вывод. Но похоже, что это единственный способ, который мог бы сделать это, если вывод был добавлен к исходному входу или, возможно, копия исходного ввода. –

+0

@JimMischel Проблема заключается в 'First' (' items.First'), и она будет вызываться хотя бы один раз, потому что 'Select' выполняет итерацию по исходной последовательности - используйте' argItems' и 'localItems' для имен, чтобы увидеть их больше clear (будет 'localItems.First ...') –

+3

Таким образом, он не потребляет свой собственный результат (т. е. создает бесконечную последовательность элементов), но вместо этого пытается найти первый элемент для перебора? Интересно . , , –

20

Похоже Select итерация над своим собственным выходом

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

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

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

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

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

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

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

Но если запрос установлен на итерацию по его собственному выходу, не должен ли результат быть пустой последовательностью? Как он может перебирать исходную коллекцию * и * свой собственный вывод? –

+1

См. Мое обновление. Проблема связана с ссылкой на «элементы» в лямбда, которая не разрешена до выполнения запроса. –

5

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

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 за помощь.