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


Al usar expresiones lambda o métodos anónimos en C #, tenemos que ser cautelosos con el acceso a 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 causará todas las Where cláusulas en la consulta que se basará en el valor final de s.

Como se explica aquí, esto sucede porque el s variable declarada en foreach El ciclo 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 se me ocurre 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 una foreach loop no se puede usar fuera del ciclo:

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

Entonces, el compilador declara la variable de una manera que la hace altamente propensa a un error que a menudo es difícil de encontrar y depurar, sin producir 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 hayan revisado desde entonces?


1482
2018-01-17 17:21


origen


Respuestas:


El compilador declara la variable de una manera que la hace altamente propensa a un error que a menudo es difícil de encontrar y depurar, sin producir beneficios perceptibles.

Tu crítica está completamente justificada.

Discutiré este problema en detalle aquí:

Cerrando sobre la variable de lazo considerada dañina

¿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 hayan revisado desde entonces?

El último. La especificación C # 1.0 en realidad no decía si la variable de 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 eligió para colocar la variable de bucle fuera del bucle, de forma consistente 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 tomar el cambio de ruptura para arreglarlo. En C # 5, la variable de ciclo foreach será lógicamente dentro el cuerpo del bucle y, por lo tanto, los cierres recibirán una copia nueva cada vez.

los for el bucle no se cambiará, y el cambio no se "retrotraerá" a las versiones anteriores de C #. Por lo tanto, debe seguir siendo cuidadoso al usar este modismo.


1279
2018-01-17 17:56



Lo que estás preguntando está completamente cubierto por Eric Lippert en su publicación de blog Cerrando sobre la variable de lazo 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(;;)estilo loop ¿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 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

La publicación de mi blog sobre este tema: Cierre de la variable foreach en C #.


174
2018-01-17 17:39



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 tengas ese hábito, puedes evitarlo en el muy raro caso de que realmente pretendiera vincularse a los ámbitos externos. Para ser honesto, no creo haberlo hecho alguna vez.


95
2018-01-17 17:47



En C # 5.0, este problema es fijo 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 colocación de v dentro del ciclo while es importante para cómo es   capturado por cualquier función anónima que ocurra en el   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 fue declarado fuera del ciclo while, sería compartido   entre todas las iteraciones, y su valor después del bucle 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 único   Capturado por f en la primera iteración continuará manteniendo el valor    7, que es lo que se imprimirá (Nota: versiones anteriores de C #   declarado v fuera del ciclo while.)


52
2017-09-03 13:58



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 código que depende del algoritmo de compilación, es una mala práctica. Y es mejor reescribir el código para excluir esta dependencia.

Esta es una buena pregunta para la entrevista de trabajo. Pero en la vida real no me enfrenté a ningún problema que resolví en una entrevista de trabajo.

El 90% de los usos foreach procesan cada elemento de la colección (no para seleccionar o calcular algunos valores). a veces necesita calcular algunos valores dentro del ciclo, pero no es una buena práctica crear un BIG Bucle.

Es mejor usar expresiones LINQ para calcular los valores. Porque cuando calcules muchas cosas dentro del ciclo, después de 2-3 meses cuando tú (u otra persona) leerán este código, la persona no entenderá qué es esto y cómo debería funcionar.


0
2018-06-15 07:31