Business
Jobs
  • About Us
  • Solutions
    • Job Postings
      Post your job and receive qualified candidates in 48h.
    • Candidate Assessments
      500+ technical and psychological tests, plus anti-fraud.
    • Headhunting
      Tailor-made executive search from start to finish.
    • Payroll + EOR
      Payroll dispersal and EOR across 15+ LATAM countries.
  • Pricing
  • Jobs

0

342
Views
¿Por qué se captura una variable de alcance de clase cuando se usa un método asíncrono pero no cuando se usa una Action<T> (ejemplos de código dentro)?

Mientras paseaba al perro, estaba pensando en Action<T> , Func<T> , Task<T> , async/await (sí, nerd, lo sé...) y construí un pequeño programa de prueba en mi mente y me preguntaba qué la respuesta seria. Me di cuenta de que no estaba seguro del resultado, así que creé dos pruebas simples.

Aquí está la configuración:

  • Tengo una variable de ámbito de clase (cadena).
  • Se le asigna un valor inicial.
  • La variable se pasa como parámetro a un método de clase.
  • El método no se ejecutará directamente, sino que se asignará a una 'Acción'.
  • Antes de que se ejecute la acción, cambio el valor de la variable.

¿Cuál sería la salida? ¿El valor inicial o el valor modificado?

Un poco sorprendente pero comprensible, la salida es el valor modificado. Mi explicación: la variable no se coloca en la pila hasta que se ejecuta la acción, por lo que será la modificada.

 public class foo { string token; public foo () { this.token = "Initial Value"; } void DoIt(string someString) { Console.WriteLine("SomeString is '{0}'", someString); } public void Run() { Action op = () => DoIt(this.token); this.token = "Changed value"; // Will output "Changed value". op(); } }

A continuación, creé una variación:

 public class foo { string token; public foo () { this.token = "Initial Value"; } Task DoIt(string someString) { // Delay(0) is just there to try if timing is the issue here - can also try Delay(1000) or whatever. return Task.Delay(0).ContinueWith(t => Console.WriteLine("SomeString is '{0}'", someString)); } async Task Execute(Func<Task> op) { await op(); } public async void Run() { var op = DoIt(this.token); this.token = "Changed value"; // The output will be "Initial Value"! await Execute(() => op); } }

Aquí hice que DoIt() devolviera una Task . op ahora es una Task y ya no es una Action . El método Execute() espera la tarea. Para mi sorpresa, la salida ahora es "Valor inicial".

¿Por qué se comporta diferente?

DoIt() no se ejecutará hasta que se llame a Execute() , entonces, ¿por qué captura el valor inicial del token ?

Pruebas completas: https://gist.github.com/Krumelur/c20cb3d3b4c44134311f y https://gist.github.com/Krumelur/3f93afb50b02fba6a7c8

over 4 years ago · Santiago Trujillo
3 answers
Answer question

0

Tienes un par de conceptos erróneos aquí. En primer lugar, cuando llama a DoIt , devuelve una tarea que ya ha comenzado a ejecutarse. La ejecución no comienza solo cuando await la Tarea.

También crea un cierre sobre la variable someString , cuyo valor no cambia cuando reasigna el campo de nivel de clase:

 Task DoIt(string someString) { return Task.Delay(0).ContinueWith(t => Console.WriteLine("SomeString is '{0}'", someString)); }

La Action pasada a ContinueWith se cierra en la variable someString . Recuerde que las cadenas son inmutables , por lo que cuando reasigna el valor de token , en realidad está asignando una nueva referencia de cadena . Sin embargo, la variable local someString dentro de DoIt conserva la referencia anterior, por lo que su valor sigue siendo el mismo incluso después de reasignar el campo de clase.

Podría resolver este problema haciendo que esta acción se cierre directamente sobre el campo de nivel de clase:

 Task DoIt() { return Task.Delay(0).ContinueWith(t => Console.WriteLine("SomeString is '{0}'", this.token)); }
over 4 years ago · Santiago Trujillo Report

0

En ambos casos, estás haciendo un cierre. Sin embargo, estás cerrando cosas diferentes en los dos casos.

En el primer caso, está creando un método anónimo con un cierre sobre this ; cuando finalmente ejecute el delegado, tomará el valor actual de this , obtendrá el valor actual de this.token y lo usará. Entonces ves el valor modificado.

En el segundo caso, no hay cierre sobre this , o si lo hay, no hace la diferencia. this.token explícitamente, y el método DoIt solo necesita hacer un cierre sobre su propio argumento, someString . Esto sucede de inmediato (sincrónicamente), en lugar de perezosamente, por lo que se captura el valor inicial de this.token . await en realidad no ejecuta el delegado, solo espera los resultados del método asíncrono. El método en sí ya se ejecutó, y solo su parte asíncrona es, bueno, asíncrona; en este caso, solo Console.WriteLine("SomeString is '{0}'", someString) .

Si desea ver esto más claramente, agregue Thread.Sleep(1000) después de this.token = "Changed value"; - Verá que SomeString is 'Initial Value' impreso incluso antes de llegar a la await .

Para que el segundo ejemplo se comporte como el primero, todo lo que necesita hacer es cambiar op para que sea un delegado nuevamente, en lugar de una Task - var op = () => DoIt(this.token); . Esto retrasa la ejecución de DoIt nuevamente y provoca el mismo cierre que en el primer ejemplo.

TL;RD:

El comportamiento es diferente porque en el primer caso, aplazas la ejecución de DoIt(this.token) , mientras que en el segundo ejemplo ejecutas DoIt(this.token) inmediatamente. Los otros puntos en mi respuesta también son importantes, pero este es el quid.

over 4 years ago · Santiago Trujillo Report

0

Desglosemos cada caso.

Comenzando con la Action<T> :

Mi explicación: la variable no se coloca en la pila hasta que se ejecuta la acción, por lo que será la modificada

Esto no tiene nada que ver con la pila. El compilador genera lo siguiente a partir de su primer fragmento de código:

 public foo() { this.token = "Initial Value"; } private void DoIt(string someString) { Console.WriteLine("SomeString is '{0}'", someString); } public void Run() { Action action = new Action(this.<Run>b__3_0); this.token = "Changed value"; action(); } [CompilerGenerated] private void <Run>b__3_0() { this.DoIt(this.token); }

El compilador emite un método con nombre de su expresión lambda. Una vez que invoque la acción, y dado que estamos en la misma clase, this.token es el "Valor modificado" actualizado. El compilador ni siquiera eleva esto a una clase de visualización, ya que todo esto se crea e invoca dentro del método de instancia.


Ahora, para el método async . Se están generando dos máquinas de estado, escatimaré la hinchazón de la máquina de estado y llegaré a las partes relevantes. La máquina de estado hace lo siguiente:

 this.<>8__1 = new foo.<>c__DisplayClass4_0(); this.<>8__1.op = this.<>4__this.DoIt(this.<>4__this.token); this.<>4__this.token = "Changed value"; taskAwaiter = this.<>4__this.Execute(new Func<Task>(this.<>8__1.<Run>b__0)).GetAwaiter();

¿Qué pasa aquí? token se pasa a DoIt , que devolverá un Func<Task> . Ese delegado contiene una referencia a la cadena de token anterior, "Valor inicial". Recuerde, aunque estamos hablando de tipos de referencia, todos se pasan por valor. Esto significa efectivamente que ahora hay una nueva ubicación de almacenamiento para la cadena anterior en el método DoIt que apunta a "Valor inicial". Luego, la siguiente línea cambia el token a "Valor cambiado". La string almacenada dentro de Func y la que se cambió ahora apuntan a dos cadenas diferentes .

Cuando invoque al delegado, imprimirá el valor inicial, ya que la op de operación almacena su valor antiguo y obsoleto. Es por eso que estás viendo dos comportamientos diferentes.

over 4 years ago · Santiago Trujillo Report
Answer question
Find remote jobs

Discover the new way to find a job!

Top jobs
Top job categories
Business
Post vacancy Pricing Sales
Legal
Terms and conditions Privacy policy
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Show me some job opportunities
There's an error!