query - ¿Hay alguna razón para que C#reutilice la variable en un foreach?
query lambda c# (5)
El compilador declara la variable de una manera que lo hace altamente propenso a un error que a menudo es difícil de encontrar y depurar, mientras que no produce beneficios perceptibles.
Su crítica está totalmente justificada.
Discuto este problema en detalle aquí:
Cierre sobre la variable de bucle considerada dañina.
¿Hay algo que pueda hacer con los bucles foreach de esta manera que no podría hacer si se compilaran con una variable de ámbito interno? ¿o es solo una elección arbitraria que se realizó antes de que los métodos anónimos y las expresiones lambda estuvieran disponibles o fueran comunes, y que no se hayan revisado desde entonces?
El último. La especificación C # 1.0 en realidad no dijo si la variable del bucle estaba dentro o fuera del cuerpo del bucle, ya que no hizo una diferencia observable. Cuando se introdujo la semántica de cierre en C # 2.0, se tomó la decisión de colocar la variable de bucle fuera del bucle, de manera consistente con el bucle "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 decisivo 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 bucle for
no se modificará, y el cambio no se "adaptará" a las versiones anteriores de C #. Por lo tanto, debe seguir teniendo cuidado al utilizar este idioma.
Cuando se utilizan expresiones lambda o métodos anónimos en C #, debemos tener cuidado con el acceso a la trampa de cierre modificada . 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 cláusulas Where
en la consulta se basen en el valor final de s
.
Como se explica here , esto sucede porque la variable s
declarada en el bucle foreach
anterior se traduce así en el compilador:
string s;
while (enumerator.MoveNext())
{
s = enumerator.Current;
...
}
en lugar de como esto:
while (enumerator.MoveNext())
{
string s;
s = enumerator.Current;
...
}
Como se señaló here , no hay ventajas de rendimiento al declarar una variable fuera del bucle, y en circunstancias normales, la única razón que se me ocurre 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 bucle foreach
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 altamente propenso a un error que a menudo es difícil de encontrar y depurar, mientras que no produce beneficios perceptibles.
¿Hay algo que pueda hacer con los bucles foreach
esta manera que no podría hacer si se compilaran con una variable de ámbito interno, o es solo una elección arbitraria que se realizó 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?
En C # 5.0, este problema está solucionado 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 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 ubicación de
v
dentro del bucle while es importante por cómo 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
v
fuera declarado fuera del bucle while, se compartiría entre todas las iteraciones, y su valor después del bucle for sería el valor final,13
, que es lo que se imprimirá la invocación def
. En cambio, como cada iteración tiene su propia variablev
, la capturada porf
en la primera iteración continuará manteniendo el valor7
, que es lo que se imprimirá. ( Nota: las versiones anteriores de C # se declararonv
fuera del bucle while ) .
En mi opinión es una pregunta extraña. Es bueno saber cómo funciona el compilador, pero eso solo es "bueno saberlo".
Si escribe un código que depende del algoritmo del compilador, es una mala práctica. Y es mejor volver a escribir el código para excluir esta dependencia.
Esta es una buena pregunta para la entrevista de trabajo. Pero en la vida real no he enfrentado ningún problema que resolviera en una entrevista de trabajo.
El 90% de los usos de foreach para procesar cada elemento de la colección (no para seleccionar o calcular algunos valores). a veces es necesario calcular algunos valores dentro del bucle, pero no es una buena práctica crear un bucle GRANDE.
Es mejor usar expresiones LINQ para calcular los valores. Porque cuando calculas muchas cosas dentro del bucle, después de 2-3 meses cuando (o cualquier otra persona) lea este código, la persona no entenderá qué es esto y cómo debería funcionar.
Habiendo sido mordido por esto, tengo el hábito 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 muy raro caso en el que realmente pretendía vincularse a los ámbitos externos. Para ser honesto, no creo que lo haya hecho nunca.
Lo que estás pidiendo está cubierto completamente por Eric Lippert en su 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 bucle de estilo for(;;)
. ¿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 hacer 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 tema: Cierre sobre la variable foreach en C # .