Acabo de encontrar un comportamiento extraño al usar métodos asíncronos en estructuras. ¿Alguien puede explicar por qué sucede esto y, lo que es más importante, si hay una solución? Aquí hay una estructura de prueba simple solo por el bien de una demostración del problema
public struct Structure { private int _Value; public Structure(int iValue) { _Value = iValue; } public void Change(int iValue) { _Value = iValue; } public async Task ChangeAsync(int iValue) { await Task.Delay(1); _Value = iValue; } }Ahora, usemos la estructura y hagamos las siguientes llamadas
var sInstance = new Structure(25); sInstance.Change(35); await sInstance.ChangeAsync(45); La primera línea instancia la estructura y el valor de sInstance._Value es 25 . La segunda línea actualiza el valor sInstance._Value y se convierte en 35 . Ahora, la tercera línea no hace nada, pero espero que actualice el valor de sInstance._Value a 45 , sin embargo sInstance._Value permanece en 35 . ¿Por qué? ¿Hay alguna manera de escribir un método asíncrono para una estructura y cambiar el valor de un campo de estructura?
¿Por qué?
Debido a la forma en que su struct se eleva a la máquina de estado.
Así es como se ve ChangeAsync en realidad :
[DebuggerStepThrough, AsyncStateMachine(typeof(Program.Structure.<ChangeAsync>d__4))] public Task ChangeAsync(int iValue) { Program.Structure.<ChangeAsync>d__4 <ChangeAsync>d__; <ChangeAsync>d__.<>4__this = this; <ChangeAsync>d__.iValue = iValue; <ChangeAsync>d__.<>t__builder = AsyncTaskMethodBuilder.Create(); <ChangeAsync>d__.<>1__state = -1; AsyncTaskMethodBuilder <>t__builder = <ChangeAsync>d__.<>t__builder; <>t__builder.Start<Program.Structure.<ChangeAsync>d__4>(ref <ChangeAsync>d__); return <ChangeAsync>d__.<>t__builder.Task; }La línea importante es esta:
<ChangeAsync>d__.<>4__this = this;El compilador eleva una copia de su estructura a su máquina de estado, actualizando efectivamente su copia con el valor 45. Cuando se completa el método asíncrono, ha mutado la copia, mientras que la instancia de su estructura sigue siendo la misma.
Este es un comportamiento algo esperado cuando se trata de estructuras mutables. Es por eso que tienden a ser malvados .
¿Cómo resuelves esto? Como no veo que este comportamiento cambie, tendrás que crear una class en lugar de una struct .
Editar:
Publicado esto como un problema en GitHub . Recibí una respuesta bien educada de @AlexShvedov, que explica un poco más en profundidad la complejidad de las estructuras y las máquinas de estado:
Dado que la ejecución de cada cierre se puede retrasar arbitrariamente, necesitamos alguna forma de retrasar también la vida útil de todos los miembros capturados en el cierre. No hay forma de hacerlo en general para este tipo de valor, ya que el tipo de valor se puede asignar en la pila (variables locales de tipos de valor) y el espacio de la pila se reutilizará al salir de la ejecución del método.
En teoría, cuando el tipo de valor se almacena como un campo de algún objeto/elemento administrado de la matriz, C# puede emitir un código de cierre para realizar una mutación de estructura en su lugar. Desafortunadamente, no se sabe dónde se encuentra este valor cuando se emite el código de miembro de la estructura, por lo que C# simplemente decidió obligar a los usuarios a manejar esta situación manualmente (copiando este valor la mayor parte del tiempo, como sugiere el mensaje de error).