¿Hay alguna razón para la reutilización de C # de la variable en un foreach?

1685

Cuando se utilizan expresiones lambda o métodos anónimos en C #, debemos ser cautelosos con respecto al acceso a errores de cierre modificados . 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 hará que todas las Wherecláusulas de la consulta se basen en el valor final de s.

Como se explica aquí , esto sucede porque la svariable declarada en el foreachbucle anterior se traduce así en el compilador:

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

en lugar de esto:

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

Como se señaló aquí , no hay ventajas de rendimiento para declarar una variable fuera del ciclo, y en circunstancias normales, la única razón que puedo pensar para hacerlo es si planea usar la variable fuera del alcance del ciclo:

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

Sin embargo, las variables definidas en un foreachbucle no pueden usarse 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 lo hace muy propenso a un error que a menudo es difícil de encontrar y depurar, sin producir beneficios perceptibles.

¿Hay algo que puede hacer con los foreachbucles de esta manera que no podría si se compilaran con una variable de alcance 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 ¿ha sido revisado desde entonces?

StriplingWarrior
fuente
44
¿Qué tiene de malo String s; foreach (s in strings) { ... }?
Brad Christie
55
@BradChristie, el OP no está hablando realmente foreachsino sobre expresiones lamda que resultan en un código similar al mostrado por el OP ...
Yahia
22
@BradChristie: ¿Eso compila? ( Error: el tipo y el identificador son necesarios en una declaración foreach para mí)
Austin Salonen
32
@JakobBotschNielsen: Es un local externo cerrado de una lambda; ¿por qué estás suponiendo que va a estar en la pila? ¡Su vida útil es más larga que el marco de la pila !
Eric Lippert
3
@EricLippert: estoy confundido. Entiendo que lambda captura una referencia a la variable foreach (que se declara internamente fuera del ciclo) y, por lo tanto, termina comparando con su valor final; que tengo Lo que no entiendo es cómo declarar la variable dentro del bucle hará alguna 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 quisiera insertar una nueva referencia en la pila cada iteración!
Anthony

Respuestas:

1407

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, sin producir beneficios perceptibles.

Su crítica está completamente justificada.

Discuto este problema en detalle aquí:

Cerrar sobre la variable de bucle considerado dañino

¿Hay algo que pueda hacer con los bucles foreach de esta manera que no podría hacer si se compilaran con una variable de alcance 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 C # 1.0 en realidad no decía si la variable del bucle estaba dentro o fuera del cuerpo del bucle, ya que no había ninguna diferencia observable. Cuando se introdujo la semántica de cierre en C # 2.0, se tomó la decisión de colocar la variable del bucle fuera del bucle, de manera consistente con el bucle "for".

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

El forbucle no se cambiará y el cambio no se "trasladará" a versiones anteriores de C #. Por lo tanto, debe seguir teniendo cuidado al usar este idioma.

Eric Lippert
fuente
177
De hecho, rechazamos 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 muchas lambdas (y consultas comprensiones, que son lambdas disfrazadas) en bucles foreach gracias a LINQ. Lamento que hayamos esperado que el problema empeorara lo suficiente como para justificar arreglarlo tan tarde, en lugar de arreglarlo en C # 3.
Eric Lippert
75
Y ahora tendremos que recordar que foreaches 'seguro' pero forno lo es.
leppie
22
@michielvoo: El cambio se está rompiendo en el sentido de que no es compatible con versiones anteriores. El nuevo código no se ejecutará correctamente cuando se utiliza un compilador anterior.
leppie
41
@Benjol: No, por eso estamos dispuestos a aceptarlo. Sin embargo, Jon Skeet me señaló un escenario de cambio importante, que es que alguien escribe el código en C # 5, lo prueba y luego lo comparte con personas que todavía usan C # 4, que luego ingenuamente creen que es correcto. Esperemos que la cantidad de personas afectadas por este escenario sea pequeña.
Eric Lippert el
29
Por otro lado, ReSharper siempre ha captado esto y lo informa como "acceso al cierre modificado". Luego, presionando Alt + Enter, incluso arreglará automáticamente su código por usted. jetbrains.com/resharper
Mike Chamberlain
191

Lo que estás preguntando está completamente cubierto por Eric Lippert en su publicación de blog Cierre sobre la variable de bucle considerada dañina y su secuela.

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

El problema más común con este comportamiento es cerrar 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 tema: Cierre sobre la variable foreach en C # .

Krizz
fuente
18
En definitiva, lo que la gente realmente quiere cuando escribe esto no es tener múltiples variables, es cerrar el valor . Y es difícil pensar en una sintaxis utilizable para eso en el caso general.
Random832
1
Sí, no es posible cerrar por el valor, sin embargo, hay una solución muy fácil que acabo de editar mi respuesta para incluir.
Krizz
66
Es una lástima que los cierres en C # se cierren sobre las referencias. Si se cerraron sobre los valores de forma predeterminada, podríamos especificar fácilmente el cierre sobre las variables en su lugar con ref.
Sean U
2
@ Krizz, este es un caso donde la consistencia forzada es más dañina que inconsistente. Debería "simplemente funcionar" como la gente espera, y claramente las personas esperan algo diferente cuando usan foreach en lugar de un bucle for, dada la cantidad de personas que han tenido problemas antes de que supiéramos sobre el acceso al problema de cierre modificado (como yo) .
Andy
2
@ Random832 no sabe acerca de C #, pero en Common LISP hay una sintaxis para eso, y calcula que cualquier lenguaje con variables mutables y cierres (no, debe ) tener también. O cerramos sobre la referencia al lugar cambiante, o un valor que tiene en un momento dado en el tiempo (creación de un cierre). Esto discute cosas similares en Python y Scheme ( cutpara referencias / vars y cutepara mantener los valores evaluados en cierres parcialmente evaluados).
Will Ness
103

Habiendo sido mordido por esto, tengo la costumbre de incluir variables definidas localmente en el ámbito más interno que utilizo 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 muy raro en el que realmente pretendía unirse a los ámbitos externos. Para ser sincero, no creo haberlo hecho nunca.

Godeke
fuente
24
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 mucho tiempo, pero dado que es, en palabras de Eric Lippert, "el informe de error incorrecto más común que obtenemos", tenía curiosidad por saber por qué más que cómo evitarlo .
StriplingWarrior
62

En C # 5.0, este problema se soluciona y puede cerrar las variables de bucle y obtener los resultados que espera.

La especificación del lenguaje dice:

8.8.4 La declaración foreach

(...)

Una declaración foreach de la forma

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 ubicación vdentro del ciclo while es importante para la forma en que es capturada por cualquier función anónima que ocurra en la declaració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();

Si vse declarara 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 fque imprimiría la invocación de . En cambio, debido a que cada iteración tiene su propia variable v, la capturada fen la primera iteración continuará manteniendo el valor 7, que es lo que se imprimirá. ( Nota: versiones anteriores de C # declaradas vfuera del bucle while ) .

Paolo Moretti
fuente
1
¿Por qué esta versión inicial de C # declaró v dentro del ciclo while? msdn.microsoft.com/en-GB/library/aa664754.aspx
colinfang
44
@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 ) en realidad no dijo si la variable del bucle estaba dentro o fuera del cuerpo del bucle, ya que no hace una diferencia observable . Cuando se introdujo la semántica de cierre en C # 2.0, se tomó la decisión de colocar la variable del bucle fuera del bucle, de manera consistente con el bucle "for".
Paolo Moretti
1
Entonces, ¿estás diciendo que los ejemplos en el enlace no eran especificaciones definitivas en ese momento?
colinfang
44
@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 apareció C # 2.0, decidieron poner la variable del bucle fuera del bucle. Y luego cambiaron de opinión nuevamente con C # 5.0 :)
Paolo Moretti