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 lositems
se vuelven a asignar, lositems
antiguos se han iterado, por lo que termina con unaList<int>
simple en memoriaList<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.