tiempo infinito ejemplo ciclo bucle c# linq stack-overflow infinite-loop

c# - ejemplo - ¿Por qué este método da como resultado un bucle infinito?



bucle infinito tiempo (3)

Parece que Select está iterando sobre su propia salida

Estás en lo correcto. Está devolviendo una consulta que itera sobre sí misma.

La clave es que haga referencia a items dentro de la lambda . La referencia de items no se resuelve ("cerrado") hasta que la consulta se repite, momento en el que los items ahora hacen referencia a la consulta en lugar de la colección de origen. Ahí es donde ocurre la autorreferencia.

Imagine una baraja de cartas con un letrero enfrente etiquetado como items . Ahora imagine a un hombre parado al lado del mazo de cartas cuya tarea es iterar la colección llamada items . Pero luego mueves el cartel de la cubierta al hombre . Cuando le preguntas al hombre por el primer "artículo", él busca la colección marcada como "artículos", ¡que ahora es él! Entonces se pregunta por el primer elemento, que es donde ocurre la referencia circular.

Cuando asigna el resultado a una nueva variable, tiene una consulta que itera sobre una colección diferente y, por lo tanto, no genera un bucle infinito.

Cuando llama a ToList , hidrata la consulta a una nueva colección y tampoco obtiene un bucle infinito.

Otras cosas que romperían la referencia circular:

  • Hidratar elementos dentro de la lambda llamando a ToList
  • Asignando items a otra variable y haciendo referencia a eso dentro de la lambda.

Uno de mis compañeros de trabajo vino a mí con una pregunta sobre este método que resulta en un ciclo infinito. El código real es demasiado complicado para publicarlo aquí, pero esencialmente el problema se reduce a esto:

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

Esto (debería pensar) debería ser una forma muy ineficiente de crear una copia de una lista. Lo llamé con:

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

El resultado es un bucle infinito. Extraño.

Creo que modificar el parámetro es, estilísticamente, algo malo, así que cambié ligeramente el código:

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

Eso funciono. Es decir, el programa completado; sin excepción.

Más experimentos mostraron que esto también funciona:

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

Como hace un simple

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

Curioso.

Está claro que el problema tiene que ver con reasignar el parámetro, pero solo si la evaluación se difiere más allá de esa declaración. Si agrego ToList() funciona.

Tengo una idea general, vaga, de lo que va mal. Parece que Select está iterando sobre su propia salida. Eso es un poco extraño en sí mismo, porque generalmente un IEnumerable arrojará si la colección que está iterando cambia.

Lo que no entiendo, porque no estoy íntimamente familiarizado con lo interno de cómo funciona esto, es por qué reasignar el parámetro causa este bucle infinito.

¿Hay alguien con más conocimiento de lo interno que estaría dispuesto a explicar por qué ocurre el bucle infinito aquí?


Después de estudiar las dos respuestas dadas y hurgar un poco, se me ocurrió un pequeño programa que ilustra mejor el problema.

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; }

Si llamas a eso con:

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

Obtendrá esta salida repetidamente hasta que finalmente obtenga Exception .

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

Lo que esto muestra es exactamente lo que dasblinkenlight dejó claro en su respuesta actualizada: la consulta entra en un bucle infinito tratando de obtener el primer elemento.

Escribamos GoNuts una manera ligeramente diferente:

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; }

Si ejecuta eso, tiene éxito. ¿Por qué? Porque en este caso está claro que la llamada a GetFirst está pasando una referencia a los elementos originales que se pasaron al método. En el primer caso, GetFirst está pasando una referencia a la nueva colección de items , que aún no se ha realizado. A su vez, GetFirst dice: "Oye, necesito enumerar esta colección". Y así comienza la primera llamada recursiva que finalmente conduce a Exception .

Curiosamente, estaba en lo cierto y equivocado cuando dije que estaba consumiendo su propia producción. El Select está consumiendo la entrada original, como era de esperar. El First está tratando de consumir la salida.

Muchas lecciones que aprender aquí. Para mí, lo más importante es "no modificar el valor de los parámetros de entrada".

Gracias a dasblinkenlight, D Stanley y Lucas Trzesniewski por su ayuda.


La clave para responder esto es la ejecución diferida . Cuando haces esto

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

no itera la matriz de items pasados ​​al método. En su lugar, le asigna un nuevo IEnumerable<int> , que hace referencia a sí mismo y comienza a iterar solo cuando la persona que llama comienza a enumerar los resultados.

Es por eso que todas sus otras soluciones han solucionado el problema: todo lo que tenía que hacer es dejar de alimentar a IEnumerable<int> nuevo:

  • El uso de var foo rompe la autorreferencia usando una variable diferente,
  • Uso de elementos return items.Select... rompe la autorreferencia al no usar variables intermedias en absoluto,
  • El uso de ToList() interrumpe la autorreferencia al evitar la ejecución diferida: en el momento en que los items se vuelven a asignar, los items antiguos se han iterado, por lo que termina con una List<int> simple en memoria List<int> .

Pero si se alimenta de sí mismo, ¿cómo obtiene algo?

Así es, ¡no recibe nada! En el momento en que intentas iterar items y pedirle el primer elemento, la secuencia diferida le pide a la secuencia que se le alimente el primer elemento a procesar, lo que significa que la secuencia se está preguntando por el primer elemento a procesar. En este punto, se trata de tortugas hasta el fondo , porque para devolver el primer elemento a procesar, la secuencia primero debe hacer que el primer elemento se procese por sí mismo.