Acceso a cada variable en advertencia de cierre

86

Recibo la siguiente advertencia:

Acceso a cada variable en cierre. Puede tener un comportamiento diferente cuando se compila con diferentes versiones del compilador.

Así es como se ve en mi editor:

mensaje de error mencionado anteriormente en una ventana emergente flotante

Sé cómo solucionar esta advertencia, pero quiero saber por qué recibiría esta advertencia.

¿Se trata de la versión "CLR"? ¿Está relacionado con "IL"?

Jeroen
fuente
1
TL; Respuesta de DR: agregue .ToList () o .ToArray () al final de su expresión de consulta y se eliminará la advertencia
JoelFan

Respuestas:

136

Hay dos partes en esta advertencia. El primero es...

Acceso a cada variable en cierre

... que no es inválido per se pero es contraintuitivo a primera vista. También es muy difícil hacerlo bien. (Tanto es así que el artículo que enlazo a continuación describe esto como "dañino").

Tome su consulta y observe que el código que ha extraído es básicamente una forma expandida de lo que genera el compilador de C # (antes de C # 5) para foreach1 :

[No] entiendo por qué [lo siguiente] no es válido:

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

Bueno, es válido sintácticamente. Y si todo lo que está haciendo en su ciclo es usar el valor de, sentonces todo está bien. Pero cerrarse sconducirá a un comportamiento contrario a la intuición. Eche un vistazo al siguiente código:

var countingActions = new List<Action>();

var numbers = from n in Enumerable.Range(1, 5)
              select n.ToString(CultureInfo.InvariantCulture);

using (var enumerator = numbers.GetEnumerator())
{
    string s;

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

        Console.WriteLine("Creating an action where s == {0}", s);
        Action action = () => Console.WriteLine("s == {0}", s);

        countingActions.Add(action);
    }
}

Si ejecuta este código, obtendrá el siguiente resultado de consola:

Creating an action where s == 1
Creating an action where s == 2
Creating an action where s == 3
Creating an action where s == 4
Creating an action where s == 5

Esto es lo que esperabas.

Para ver algo que probablemente no espere, ejecute el siguiente código inmediatamente después del código anterior:

foreach (var action in countingActions)
    action();

Obtendrá la siguiente salida de consola:

s == 5
s == 5
s == 5
s == 5
s == 5

¿Por qué? Porque creamos cinco funciones que hacen exactamente lo mismo: imprimir el valor de s(que hemos cerrado). En realidad, son la misma función ("Imprimir s", "Imprimir s", "Imprimir s" ...).

En el punto en el que vamos a utilizarlos, hacen exactamente lo que les pedimos: imprimir el valor de s. Si observa el último valor conocido de s, verá que es 5. Entonces nos s == 5imprimen cinco veces en la consola.

Que es exactamente lo que pedimos, pero probablemente no lo que queremos.

La segunda parte de la advertencia ...

Puede tener un comportamiento diferente cuando se compila con diferentes versiones del compilador.

...Es lo que es. A partir de C # 5, el compilador genera un código diferente que "evita" que esto suceda a través deforeach .

Por lo tanto, el siguiente código producirá resultados diferentes en diferentes versiones del compilador:

foreach (var n in numbers)
{
    Action action = () => Console.WriteLine("n == {0}", n);
    countingActions.Add(action);
}

En consecuencia, también producirá la advertencia R # :)

Mi primer fragmento de código, arriba, exhibirá el mismo comportamiento en todas las versiones del compilador, ya que no lo estoy usando foreach(más bien, lo he expandido como lo hacen los compiladores anteriores a C # 5).

¿Es esto para la versión CLR?

No estoy muy seguro de lo que estás preguntando aquí.

La publicación de Eric Lippert dice que el cambio ocurre "en C # 5". Entoncespresumiblemente tienes que apuntar a .NET 4.5 o posterior con un compilador de C # 5 o posterior para obtener el nuevo comportamiento, y todo lo anterior obtiene el comportamiento anterior.

Pero para ser claros, es una función del compilador y no la versión de .NET Framework.

¿Existe relevancia con IL?

Un código diferente produce un IL diferente, por lo que en ese sentido hay consecuencias para el IL generado.

1 foreach es una construcción mucho más común que el código que ha publicado en su comentario. El problema suele surgir mediante el uso de foreach, no mediante la enumeración manual. Es por eso que los cambios foreachen C # 5 ayudan a prevenir este problema, pero no por completo.

ta.speot.is
fuente
7
De hecho, probé el bucle foreach en diferentes compiladores obteniendo diferentes resultados usando el mismo objetivo (.Net 3.5). Usé VS2010 (que a su vez usa el compilador asociado con .net 4.0, creo) y VS2012 (compilador de .net 4.5, creo). En principio, esto significa que si está utilizando VS2013 y editando un proyecto destinado a .Net 3.5 y compilándolo en un servidor de compilación que tiene instalado un marco un poco más antiguo, podría ver resultados diferentes de su programa en su máquina frente a la compilación implementada.
Ykok
Buena respuesta, pero no estoy seguro de qué tan relevante es "foreach". ¿No sucedería esto con la enumeración manual, o incluso con un simple bucle for (int i = 0; i <collection.Size; i ++)? Parece ser un problema con los cierres que salen del alcance, o más exactamente, un problema con la gente que comprende cómo se comportan los cierres cuando salen del alcance en el que se definieron en su interior.
Brad
El foreachmaterial aquí proviene del contenido de la pregunta. Tienes razón en que puede suceder de varias formas más generales.
ta.speot.is
1
¿Por qué R # todavía me advierte, no lee el marco de destino, que configuré en 4.5.
Johnny_D
1
"Así que presumiblemente tienes que apuntar a .NET 4.5 o posterior" Esta afirmación no es cierta. La versión de .NET a la que se dirige no tiene ningún efecto en esto, el comportamiento también se cambia en .NET 2.0, 3.5 y 4 si está utilizando C # 5 (VS 2012 o más reciente) para compilar. Es por eso que solo recibe esta advertencia en .NET 4.0 o anterior, si apunta a 4.5, no recibe la advertencia porque no puede compilar 4.5 en un compilador C # 4 o anterior.
Scott Chamberlain
12

La primera respuesta es genial, así que pensé en agregar una cosa.

Recibirá la advertencia porque, en su código de ejemplo, a reflectModel se le está asignando un IEnumerable, que solo se evaluará en el momento de la enumeración, y la enumeración en sí podría ocurrir fuera del ciclo si asignó reflectModel a algo con un alcance más amplio .

Si cambiaste

...Where(x => x.Name == property.Value)

a

...Where(x => x.Name == property.Value).ToList()

luego a reflectModel se le asignaría una lista definida dentro del bucle foreach, por lo que no recibiría la advertencia, ya que la enumeración definitivamente ocurriría dentro del bucle y no fuera de él.

David
fuente
Leí muchas explicaciones realmente largas que no resolvieron este problema para mí, luego una breve que lo hizo. ¡Gracias!
Charles Clayton
Leí la respuesta aceptada y pensé "¿cómo es un cierre si no vincula las variables?" pero ahora entiendo que se trata de cuándo ocurre la evaluación, ¡gracias!
Jerome
Sí, esta es una solución universal obvia. Lento, requiere mucha memoria, pero creo que realmente funciona al 100% en todos los casos.
Al Kepp
8

Una variable de ámbito de bloque debería resolver la advertencia.

foreach (var entry in entries)
{
   var en = entry; 
   var result = DoSomeAction(o => o.Action(en));
}
Dmitry Gogol
fuente