¿Existe alguna razón para la reutilización de la variable en un foreach por parte de C#?
Cuando utilizamos expresiones lambda o métodos anónimos en C#, debemos tener cuidado con el acceso al cierre modificado . 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 Where
cláusulas de la consulta se basen en el valor final de s
.
Como se explica aquí , esto sucede porque la 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ñala 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 planeas 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
bucle no se pueden utilizar 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, sin producir beneficios perceptibles.
¿Hay algo que pueda hacer con foreach
los bucles de esta manera que no podría hacer si estuvieran compilados con una variable de ámbito interno, o es simplemente 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 ¿No ha sido revisado desde entonces?
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.
Tu crítica está totalmente justificada.
Discuto este problema en detalle aquí:
Cerrar la variable del bucle se considera perjudicial
¿Hay algo que puedas hacer con los bucles foreach de esta manera que no podrías si estuvieran compilados con una variable de ámbito interno? ¿O es simplemente 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?
Este ú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 hacía ninguna diferencia observable. Cuando se introdujo la semántica de cierre en C# 2.0, se optó por colocar la variable del bucle fuera del bucle, de forma coherente con el bucle "for".
Creo que es justo decir que todos lamentamos esa decisión. Este es uno de los peores "errores" en C#, y vamos a realizar el cambio importante 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 for
bucle no se cambiará y el cambio no se "portará" a versiones anteriores de C#. Por lo tanto, debes seguir teniendo cuidado al utilizar este modismo.
Eric Lippert cubre detalladamente lo que está preguntando en su publicación de blog Cerrar 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 for(;;)
el bucle de estilo. ¿ Esperaría 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
Publicación de mi blog sobre este tema: Cierre sobre la variable foreach en C# .
Habiendo sido mordido por esto, tengo la costumbre de incluir variables definidas localmente en el alcance 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
Sí:
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, podrá evitarlo en el muy raro caso de que realmente pretenda vincularse a los ámbitos externos. Para ser honesto, creo que nunca lo he hecho.
En C# 5.0, este problema está solucionado y puede cerrar las variables del bucle y obtener los resultados esperados.
La especificación del idioma 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
v
dentro del bucle while es importante para saber cómo lo captura cualquier función anónima que se produzca 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
v
se 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, que es lo que se imprimiría13
al invocarlo .f
En cambio, debido a que cada iteración tiene su propia variablev
, la capturadaf
en la primera iteración seguirá manteniendo el valor7
, que es lo que se imprimirá. ( Nota: versiones anteriores de C# declaradasv
fuera del bucle while ) .