2012-06-15 11 views
14

acabo encontró el siguiente comportamiento:¿Cómo puedo capturar el valor de una variable externa dentro de una expresión lambda?

for (var i = 0; i < 50; ++i) { 
    Task.Factory.StartNew(() => { 
     Debug.Print("Error: " + i.ToString()); 
    }); 
} 

dará lugar a una serie de "Error: x", donde la mayor parte de los X son iguales a 50.

mismo modo:

var a = "Before"; 
var task = new Task(() => Debug.Print("Using value: " + a)); 
a = "After"; 
task.Start(); 

Causará "Uso de valor: Después".

Esto significa claramente que la concatenación en la expresión lambda no ocurre inmediatamente. ¿Cómo es posible usar una copia de la variable externa en la expresión lambda, en el momento en que se declara la expresión? El siguiente no va a funcionar mejor (que no es necesariamente incoherente, lo admito):

var a = "Before"; 
var task = new Task(() => { 
    var a2 = a; 
    Debug.Print("Using value: " + a2); 
}); 
a = "After"; 
task.Start(); 
+0

¿Por qué habrían de hacerlo? Son asíncronos de todos modos. – Vlad

+0

Posible duplicado de "C# Captured Variable in Loop" http://stackoverflow.com/questions/271440/c-sharp-captured-variable-in-loop –

+0

En mi humilde opinión, terminas haciendo 2 preguntas aquí - aparece el 'real' estar en el título (cómo capturar el valor, de modo que la tarea se ejecute sobre el valor en tiempo de ciclo), pero luego el cuerpo de la pregunta parece enfocarse en "¿por qué estas cosas resultan en valores inesperados?" (el efecto de la captura de cierre significa que todos hacen referencia a la misma variable). Por lo tanto, terminas con la mayoría de las respuestas explicando el comportamiento en lugar de responder a tu pregunta "real" (AFAICT :) –

Respuesta

23

Esto tiene más que ver con lambdas que el roscado. Una lambda captura la referencia a una variable, no el valor de la variable. Esto significa que cuando intente utilizar i en su código, su valor será el que fue almacenado en i último.

Para evitar esto, debe copiar el valor de la variable a una variable local cuando se inicia el lambda. El problema es que iniciar una tarea tiene una sobrecarga y la primera copia puede ejecutarse solo después de que termine el ciclo. El siguiente código también fallará

for (var i = 0; i < 50; ++i) { 
    Task.Factory.StartNew(() => { 
     var i1=i; 
     Debug.Print("Error: " + i1.ToString()); 
    }); 
} 

Como se ha señalado James Manning, se puede añadir una variable local al bucle y copiar la variable de bucle allí. De esta forma estás creando 50 variables diferentes para mantener el valor de la variable de ciclo, pero al menos obtienes el resultado esperado. El problema es que obtienes muchas asignaciones adicionales.

for (var i = 0; i < 50; ++i) { 
    var i1=i; 
    Task.Factory.StartNew(() => { 
     Debug.Print("Error: " + i1.ToString()); 
    }); 
} 

La mejor solución es pasar el parámetro de bucle como un parámetro de estado:

for (var i = 0; i < 50; ++i) { 
    Task.Factory.StartNew(o => { 
     var i1=(int)o; 
     Debug.Print("Error: " + i1.ToString()); 
    }, i); 
} 

El uso de un parámetro de estado resultados en un menor número de asignaciones. Si examina el código descompilada:

  • el segundo fragmento creará 50 cierres y 50 delegados
  • el tercer fragmento creará 50 enteros en caja, pero sólo un único delegado
+1

La situación es bastante conocida, se describe aquí: http: //blogs.msdn.com/b/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx –

+0

La segunda parte funciona, pero no la primera, así que no puedo marcarla como una solución aceptada todavía. –

+1

Para el primer ciclo, la corrección 'correcta' (AFAICT) es hacer var i1 = i; dentro del ciclo pero antes de Task.Factory.StartNew. Con ese cambio, cada cierre se referirá a su propia variable separada y obtendrá el efecto correcto. Sin embargo, el parámetro de estado evita la necesidad del cierre, por lo que ciertamente es más eficiente, pero no es necesario si solo quiere el comportamiento correcto. –

4

Esto se debe a que está ejecutando el código en un nuevo hilo, y el hilo principal se pone inmediatamente en cambiar la variable. Si la expresión lambda se ejecutara inmediatamente, se perdería todo el punto de uso de una tarea.

El hilo no obtiene su propia copia de la variable en el momento en que se crea la tarea, todas las tareas usan la misma variable (que en realidad está almacenada en el cierre para el método, no es una variable local).

3

Las expresiones lambda capturan no el valor de la variable externa sino una referencia a ella. Esa es la razón por la que ve 50 o After en sus tareas.

Para resolver este crear antes de su expresión lambda una copia de la misma para capturarlo por valor.

Este comportamiento desafortunado será corregido por el compilador de C# con .NET 4.5 hasta entonces usted necesita vivir con esta rareza.

Ejemplo:

List<Action> acc = new List<Action>(); 
    for (int i = 0; i < 10; i++) 
    { 
     int tmp = i; 
     acc.Add(() => { Console.WriteLine(tmp); }); 
    } 

    acc.ForEach(x => x()); 
+0

¿Quiere decir que crear una copia en la expresión lambda funcionará? Actualmente no lo hace: el uso de var a2 = a; Logging.Print ("Uso del valor:" + a2); todavía recupera "Usar valor: Después". –

+0

Lo siento. Debe colocar la copia fuera de la lambda para que funcione. –

1

expresiones lambda son, por definición, perezosamente evaluadas para que no sean evaluadas hasta que realmente se llamen. En tu caso por la ejecución de la tarea. Si cierra sobre un local en su expresión lambda, se reflejará el estado del local en el momento de la ejecución. Que es lo que ves Puedes aprovechar esto. P.ej. el bucle for realmente no necesita un nuevo lambda para cada iteración suponiendo, a efectos del ejemplo, que el resultado descrito era lo que pretende usted podría escribir

var i =0; 
Action<int> action =() => Debug.Print("Error: " + i); 
for(;i<50;+i){ 
    Task.Factory.StartNew(action); 
} 

por el contrario si hubiese deseado que en realidad impreso "Error: 1"..."Error 50" podría cambiar el anterior para

var i =0; 
Func<Action<int>> action = (x) => { return() => Debug.Print("Error: " + x);} 
for(;i<50;+i){ 
    Task.Factory.StartNew(action(i)); 
} 

los primeros se cierra sobre i y usará el estado de i en el momento en que se ejecuta la acción y el estado es a menudo va a ser el estado después de que finalice el bucle. En el último caso, i se evalúa con entusiasmo porque se pasa como argumento a una función. Esta función luego devuelve un Action<int> que se pasa al StartNew.

Por lo tanto, la decisión de diseño hace que tanto la evaluación perezosa como la evaluación entusiasta sean posibles. Con pereza, porque los locales se cerraron sobre y con entusiasmo porque se puede obligar a la gente a ser ejecutado por pasarlos como un argumento o como se muestra a continuación declarando otro local con un alcance más corto

for (var i = 0; i < 50; ++i) { 
    var j = i; 
    Task.Factory.StartNew(() => Debug.Print("Error: " + j)); 
} 

Todo lo anterior es general para Lambdas. En el caso específico de StartNew hay realmente una sobrecarga que hace lo hace el segundo ejemplo de manera que se puede simplificar a

var i =0; 
Action<object> action = (x) => Debug.Print("Error: " + x);} 
for(;i<50;+i){ 
    Task.Factory.StartNew(action,i); 
} 
+0

BTW, que lambda se puede simplificar a 'x =>() => Debug.Print (" Error: "+ x)'. – svick

Cuestiones relacionadas