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

Resuelto StriplingWarrior asked hace 12 años • 4 respuestas

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 Wherecláusulas de la consulta se basen en el valor final de s.

Como se explica aquí , esto sucede porque la svariable declarada en foreachel 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 foreachbucle 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 foreachlos 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?

StriplingWarrior avatar Jan 18 '12 00:01 StriplingWarrior
Aceptado

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 forbucle 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 avatar Jan 17 '2012 17:01 Eric Lippert

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 ien 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# .

Krizz avatar Jan 17 '2012 17:01 Krizz

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.

Godeke avatar Jan 17 '2012 17:01 Godeke

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 vdentro 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 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, que es lo que se imprimiría 13al invocarlo . fEn cambio, debido a que cada iteración tiene su propia variable v, la capturada fen la primera iteración seguirá manteniendo el valor 7, que es lo que se imprimirá. ( Nota: versiones anteriores de C# declaradas vfuera del bucle while ) .

Paolo Moretti avatar Sep 03 '2012 13:09 Paolo Moretti