¿Hay alguna razón para que C # reutilice la variable en un foreach?

Al usar expresiones lambda o métodos anónimos en C #, debemos tener cuidado con el acceso al cierre modificado trampa. Por ejemplo:

foreach (var s in strings)
{
   query = query.Where(i => i.Prop == s); // access to modified closure
   ...
}

Debido al cierre modificado, el código anterior provocará que todos los Where cláusulas de la consulta que se basarán en el valor final de s.

Como se explica aquí, esto sucede porque el s variable declarada en foreach El bucle anterior se traduce así en el compilador:

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}

en lugar de así:

while (enumerator.MoveNext())
{
   string s;
   s = enumerator.Current;
   ...
}

Como se señaló aquí, no hay ventajas de rendimiento al declarar una variable fuera del bucle, y en circunstancias normales, la única razón que se me ocurre para hacer esto es si planea usar la variable fuera del alcance del bucle:

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}
var finalString = s;

Sin embargo, las variables definidas en un foreach El bucle no se puede usar fuera del bucle:

foreach(string s in strings)
{
}
var finalString = s; // won't work: you're outside the scope.

Por lo tanto, el compilador declara la variable de una manera que la hace muy propensa a un error que a menudo es difícil de encontrar y depurar, mientras que no produce beneficios perceptibles.

¿Hay algo que puedas hacer con foreach bucles de esta manera que no podría si se compilaran con una variable de ámbito interno, o es solo una elección arbitraria que se hizo antes de que los métodos anónimos y las expresiones lambda estuvieran disponibles o fueran comunes, y que no se ha revisado desde entonces?

preguntado el 17 de enero de 12 a las 13:01

Qué hay de malo en String s; foreach (s in strings) { ... }? -

@BradChristie, el OP no está hablando realmente foreachpero sobre las expresiones lamda que dan como resultado un código similar al que muestra el OP ... -

@BradChristie: ¿Eso se compila? (Error: el tipo y el identificador son obligatorios en una declaración foreach para mi) -

@JakobBotschNielsen: Es un local externo cerrado de una lambda; ¿Por qué está asumiendo que va a estar en la pila? Su vida es más largo que el marco de la pila! -

@EricLippert: Estoy confundido. Entiendo que lambda captura una referencia a la variable foreach (que se declara internamente afuera el bucle) y, por lo tanto, terminas comparando con su valor final; que consigo. Lo que no entiendo es cómo declarar la variable dentro el bucle marcará la diferencia. Desde el punto de vista del compilador-escritor, solo estoy asignando una referencia de cadena (var 's') en la pila independientemente de si la declaración está dentro o fuera del bucle; ¡Ciertamente no querría empujar una nueva referencia a la pila en cada iteración! -

4 Respuestas

El compilador declara la variable de una manera que la hace muy propensa a un error que a menudo es difícil de encontrar y depurar, mientras que no produce beneficios perceptibles.

Tu crítica está totalmente justificada.

Discuto este problema en detalle aquí:

Cerrar el bucle variable considerada perjudicial

¿Hay algo que pueda hacer con los bucles foreach de esta manera que no podría si se compilaran con una variable de ámbito interno? ¿O es solo una elección arbitraria que se hizo antes de que los métodos anónimos y las expresiones lambda estuvieran disponibles o fueran comunes, y que no se ha revisado desde entonces?

El último. La especificación de C # 1.0 en realidad no dijo si la variable de bucle estaba dentro o fuera del cuerpo del bucle, ya que no hizo ninguna diferencia observable. Cuando se introdujo la semántica de cierre en C # 2.0, se tomó la decisión de poner la variable de ciclo fuera del ciclo, de acuerdo con el ciclo "for".

Creo que es justo decir que todos lamentan esa decisión. Este es uno de los peores "errores" en C #, y vamos a tomar el cambio más importante para arreglarlo. En C # 5, la variable de bucle foreach será lógicamente dentro el cuerpo del bucle y, por lo tanto, los cierres obtendrán una copia nueva cada vez.

El for El bucle no se cambiará y el cambio no se "retrocederá" a versiones anteriores de C #. Por lo tanto, debe seguir teniendo cuidado al utilizar este idioma.

Respondido el 11 de Septiembre de 13 a las 17:09

Entonces, ¿no hay posibilidad de que una sintaxis se cierre en valores? (Por supuesto, eso tiene un problema de "nadie lo usaría", porque la sintaxis para cerrar las variables es más natural y no hace ninguna diferencia el 90% de las veces) - Random832

@ Random832: Improbable. Sin embargo, estamos considerando para Roslyn agregar un analizador estático que determina si alguna vez se escribe en la variable cerrada después de la construcción del cierre; si no es así, podríamos cerrar sobre el valor en lugar de la variable. - eric lippert

En realidad, hay una referencia indirecta en la especificación 1.x; si observa las reglas de asignación definidas, da un ejemplo de la interpretación del compilador, y IIRC se declara dentro el lazo. Sin embargo, esto es indirecto y circunstancial. No es una declaración de especificaciones explícita. - marc gravell

De hecho, hicimos retroceder este cambio en C # 3 y C # 4. Cuando diseñamos C # 3 nos dimos cuenta de que el problema (que ya existía en C # 2) iba a empeorar porque habría tantas lambdas (y consultas comprensiones, que son lambdas disfrazadas) en bucles foreach gracias a LINQ. Lamento que hayamos esperado a que el problema empeorara lo suficiente como para justificar arreglarlo tan tarde, en lugar de arreglarlo en C # 3. - eric lippert

Y ahora tendremos que recordar foreach es 'seguro' pero for no es. - leppie

Eric Lippert trata detalladamente lo que está preguntando en su publicación de blog. Cerrar el bucle variable considerada perjudicial y su secuela.

Para mí, el argumento más convincente es que tener una nueva variable en cada iteración sería inconsistente con for(;;) bucle de estilo. ¿Esperarías tener un nuevo int i en cada iteración de for (int i = 0; i < 10; i++)?

El problema más común con este comportamiento es realizar un cierre sobre la variable de iteración y tiene una solución fácil:

foreach (var s in strings)
{
    var s_for_closure = s;
    query = query.Where(i => i.Prop == s_for_closure); // access to modified closure

Mi publicación de blog sobre este problema: Cierre sobre cada variable foreach en C #.

Respondido 28 Oct 13, 21:10

En última instancia, lo que la gente realmente quieres cuando escriben, esto no debe tener múltiples variables, es para cerrar sobre el VALORAMOS. Y es difícil pensar en una sintaxis utilizable para eso en el caso general. - Random832

Habiendo sido mordido por esto, tengo la costumbre de incluir variables definidas localmente en el ámbito más interno que uso para transferir a cualquier cierre. En tu ejemplo:

foreach (var s in strings)
    query = query.Where(i => i.Prop == s); // access to modified closure

Hago:

foreach (var s in strings)
{
    string search = s;
    query = query.Where(i => i.Prop == search); // New definition ensures unique per iteration.
}        

Una vez que tenga ese hábito, puede evitarlo en el caso raro en el que realmente pretendías vincular a los ámbitos externos. Para ser honesto, no creo que lo haya hecho nunca.

Respondido el 26 de enero de 20 a las 14:01

Esa es la solución típica. Gracias por la contribución. Resharper es lo suficientemente inteligente como para reconocer este patrón y llamar su atención también, lo cual es bueno. No he sido mordido por este patrón en un tiempo, pero como es, en palabras de Eric Lippert, "el informe de error incorrecto más común que recibimos", tenía curiosidad por saber el el porqué más que el como evitarlo. - StriplingGuerrero

En C # 5.0, este problema está solucionado y puede cerrar las variables de ciclo y obtener los resultados que espera.

La especificación del lenguaje dice:

8.8.4 La declaración foreach

(...)

Una declaración foreach del formulario

foreach (V v in x) embedded-statement

luego se expande a:

{
  E e = ((C)(x)).GetEnumerator();
  try {
      while (e.MoveNext()) {
          V v = (V)(T)e.Current;
          embedded-statement
      }
  }
  finally {
      … // Dispose e
  }
}

(...)

La colocación de v dentro del bucle while es importante para saber cómo es capturado por cualquier función anónima que ocurra en la instrucción incrustada. Por ejemplo:

int[] values = { 7, 9, 13 };
Action f = null;
foreach (var value in values)
{
    if (f == null) f = () => Console.WriteLine("First value: " + value);
}
f();

If v se declaró fuera del ciclo while, se compartiría entre todas las iteraciones y su valor después del ciclo for sería el valor final, 13, que es lo que la invocación de f imprimiría. En cambio, porque cada iteración tiene su propia variable v, el capturado por f en la primera iteración continuará manteniendo el valor 7, que es lo que se imprimirá. (Nota: versiones anteriores de C # declaradas v fuera del bucle while.)

Respondido el 03 de Septiembre de 12 a las 14:09

¿Por qué esta primera versión de C # declaró v dentro del ciclo while?msdn.microsoft.com/en-GB/library/aa664754.aspx - colinfang

@colinfang Asegúrese de leer La respuesta de Eric: La especificación C # 1.0 (en su enlace estamos hablando de VS 2003, es decir, C # 1.2) Realmente no dijo si la variable de bucle estaba dentro o fuera del cuerpo del bucle, ya que no hace ninguna diferencia observable. Cuando se introdujo la semántica de cierre en C # 2.0, se tomó la decisión de poner la variable de ciclo fuera del ciclo, de acuerdo con el ciclo "for". - Paolo moretti

Entonces, ¿está diciendo que los ejemplos en el enlace no eran especificaciones definitivas en ese momento? - colinfang

@colinfang Eran especificaciones definitivas. El problema es que estamos hablando de una característica (es decir, cierres de funciones) que se introdujo más tarde (con C # 2.0). Cuando surgió C # 2.0, decidieron poner la variable de ciclo fuera del ciclo. Y luego cambiaron de opinión nuevamente con C # 5.0 :) - Paolo moretti

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.