Значение строкового частного значения не обновляется с использованием метода async

Я просто столкнулся с странным поведением с использованием методов async в структурах. Может кто-нибудь объяснить, почему это происходит и что самое главное, если есть обходной путь? Вот простая тестовая структура только для демонстрации проблемы

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;
   }
}

Теперь позвольте использовать структуру и выполните следующие вызовы

var sInstance = new Structure(25);
sInstance.Change(35);
await sInstance.ChangeAsync(45);

Первая строка создает структуру, а значение sInstance._Value равно 25. Вторая строка обновляет значение sInstance._Value и становится 35. Теперь третья строка ничего не делает, но я ожидаю, что она обновит значение sInstance._Value до 45, однако sInstance._Value останется 35. Зачем? Есть ли способ написать метод async для структуры и изменить значение поля структуры?

Ответы

Ответ 1

Почему?

Из-за того, как ваш struct поднимается на конечный автомат.

Вот что выглядит ChangeAsync:

[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;
}

Важная строка такова:

<ChangeAsync>d__.<>4__this = this;

Компилятор снимает копию вашей структуры в свою машину состояния, эффективно обновляя ее копию со значением 45. Когда метод асинхронизации завершен, он перепутал копию, а экземпляр вашего Структура остается неизменной.

Это несколько ожидаемое поведение при работе с изменяемыми структурами. Вот почему они склонны быть злыми.

Как вам обойти это? Поскольку я не вижу изменения этого поведения, вам нужно создать class вместо struct.

Edit:

Отправлено это как проблема на GitHub. Получил хорошо образованный ответ от @AlexShvedov, который объясняет немного более глубокую сложность структур и состояний машин:

Так как выполнение каждого замыкания может быть произвольно отложено, нам нужно каким-то образом задерживать жизнь всех участников, захваченных в закрытие. Нет никакого способа сделать это в целом для этого типа значения, так как тип значения может быть выделен в стеке (локальные переменные значения типы) и пространство стека будут повторно использоваться при выводе выполнения метода.

В теории, когда тип значения хранится как поле некоторого управляемого объект/элемент массива, С# может испускать код замыкания для создания структуры мутация в месте. К сожалению, нет никаких сведений о том, где это значение находится при испускании кода элемента структуры, поэтому С# просто заставить пользователей обрабатывать эту ситуацию вручную (путем копирования это значение большую часть времени, как предлагалось сообщение об ошибке).