Иногда случаются ситуации, когда ошибки появляются, как говорится, на ровном месте. Давайте рассмотрим один из таких случаев. А именно, когда использование инициализатора объекта приводит к некорректному поведению программы.
Например, код с подобной ошибкой можно найти в описании возможностей ASP.NET MVC 4:
public async Task<ActionResult> Index(string city) {
var newsService = new NewsService();
var sportsService = new SportsService();
return View("Common",
new PortalViewModel {
NewsHeadlines = await newsService.GetHeadlinesAsync(),
SportsScores = await sportsService.GetScoresAsync()
});
}
Для простоты рассмотрим в чем она состоит и её причины, используя консольное приложение. При его создании не забудьте подключить сборку Async CTP.
Инициализатор объектов + await
Предположим, что есть класс CustomData, предназначенный для хранения данных:
public class CustomData
{
public int Id { get; set; }
public string Description { get; set; }
}
Для упрощения кода объединим методы для получения его значений в CustomDataProviderAsync:
public class CustomDataProviderAsync
{
public Task<int> GetIdAsync()
{
return TaskEx.Run<int>(() => {
Thread.Sleep(200);
return 10;
});
}
public Task<string> GetDescriptionAsync()
{
return TaskEx.Run<string>(() => {
Thread.Sleep(200);
return "Description for object";
});
}
}
GetIdAsync() и GetDescriptionAsync() предназначены для вызова в асинхронном методе.
В тестовом методе создадим объект обычным способом и с использованием инициализатора:
class Program
{
static void Main(string[] args)
{
Program.ExecuteDemo();
Console.WriteLine("Press any key ...");
Console.ReadKey(intercept: true);
}
private async static void ExecuteDemo()
{
var dataProvider = new CustomDataProviderAsync();
var data1 = new CustomData();
data1.Id = await dataProvider.GetIdAsync();
data1.Description = await dataProvider.GetDescriptionAsync();
Console.WriteLine("\nStandard ===============");
Console.WriteLine("Id: {0}", data1.Id);
Console.WriteLine("Description: {0}", data1.Description);
var data2 = new CustomData() {
Id = await dataProvider.GetIdAsync(),
Description = await dataProvider.GetDescriptionAsync()
};
Console.WriteLine("\nObject initializer ===============");
Console.WriteLine("Id: {0}", data2.Id);
Console.WriteLine("Description: {0}", data2.Description);
}
}
Можно ли сказать не запуская приложение, какие какие значения будут содержаться в свойствах объектов data1 и data2? Кажется на этот вопрос легко ответить, ведь методы класса CustomDataProviderAsync возвращают константы. Но если все же посмотреть на результат выполнения этого кода, то можно увидеть:
Standard ===============
Id: 10
Description: Description for object
Object initializer ===============
Id: 0
Description: Description for object
Вот и сюрприз: при использовании инициализатора объекта, значение свойства Id равно 0, вместо 10. Используя отладку, можно убедиться, что все методы вызываются, значения передаются. Однако в Id все равно оказывается неизвестно откуда появившийся 0.
Вывод
Использовать await в инициализаторах объектов нельзя. Не смотря на то, что это не порождает сообщение об ошибке, полученный код работает не корректно. Это справедливо, как минимум, для Async CTP. Причина заключается в коде, создаваемом компилятором.
Ну а теперь, собственно, посмотрим почему это происходит.
Причина
Для выяснения причины воспользуемся .NET Reflector и заглянем в полученный exe-файл.
Не будем углубляться в принципы генерации кода, использующего Async CTP. Необходимо только отметить, что ExecuteDemo() преобразован в внутренний класс <ExecuteDemo>d__1. А за асинхронные вызовы отвечает созданный метод MoveNext(). Посмотрим на результат декомпиляции его кода.
Полученный исходный код достаточно объемный. Однако логика его работы достаточно простая. В зависимости от значения поля <>1__state происходит выполнение части кода исходного ExecuteDemo(). При этом, в моменты ожидания завершения асинхронных методов, управление возвращается вызывающему коду. Здесь можно провести аналогию с реализацией yield return.
public void MoveNext()
{
try
{
int <1>t__$await5;
string <2>t__$await7;
int <3>t__$await9;
string <4>t__$awaitb;
bool $__doFinallyBodies = true;
switch (this.<>1__state)
{
case 1:
break;
case 2:
goto Label_0127;
case 3:
goto Label_01DA;
case 4:
goto Label_0244;
default:
if (this.<>1__state != -1)
{
this.<dataProvider>5__2 = new CustomDataProviderAsync();
this.<data1>5__3 = new CustomData();
this.<a1>t__$await6 = this.<dataProvider>5__2.GetIdAsync().GetAwaiter<int>();
if (this.<a1>t__$await6.IsCompleted)
{
goto Label_00B7;
}
this.<>1__state = 1;
$__doFinallyBodies = false;
this.<a1>t__$await6.OnCompleted(this.<>t__MoveNextDelegate);
}
return;
}
this.<>1__state = 0;
Label_00B7:
<1>t__$await5 = this.<a1>t__$await6.GetResult();
TaskAwaiter<int> CS$0$0002 = new TaskAwaiter<int>();
this.<a1>t__$await6 = CS$0$0002;
this.<data1>5__3.Id = <1>t__$await5;
this.<a2>t__$await8 = this.<dataProvider>5__2.GetDescriptionAsync().GetAwaiter<string>();
if (this.<a2>t__$await8.IsCompleted)
{
goto Label_012E;
}
this.<>1__state = 2;
$__doFinallyBodies = false;
this.<a2>t__$await8.OnCompleted(this.<>t__MoveNextDelegate);
return;
Label_0127:
this.<>1__state = 0;
Label_012E:
<2>t__$await7 = this.<a2>t__$await8.GetResult();
TaskAwaiter<string> CS$0$0003 = new TaskAwaiter<string>();
this.<a2>t__$await8 = CS$0$0003;
this.<data1>5__3.Description = <2>t__$await7;
Console.WriteLine("\nStandard ===============");
Console.WriteLine("Id: {0}", this.<data1>5__3.Id);
Console.WriteLine("Description: {0}", this.<data1>5__3.Description);
this.<a3>t__$awaita = this.<dataProvider>5__2.GetIdAsync().GetAwaiter<int>();
if (this.<a3>t__$awaita.IsCompleted)
{
goto Label_01E1;
}
this.<>1__state = 3;
$__doFinallyBodies = false;
this.<a3>t__$awaita.OnCompleted(this.<>t__MoveNextDelegate);
return;
Label_01DA:
this.<>1__state = 0;
Label_01E1:
<3>t__$await9 = this.<a3>t__$awaita.GetResult();
this.<a3>t__$awaita = new TaskAwaiter<int>();
this.<a4>t__$awaitc = this.<dataProvider>5__2.GetDescriptionAsync().GetAwaiter<string>();
if (this.<a4>t__$awaitc.IsCompleted)
{
goto Label_024B;
}
this.<>1__state = 4;
$__doFinallyBodies = false;
this.<a4>t__$awaitc.OnCompleted(this.<>t__MoveNextDelegate);
return;
Label_0244:
this.<>1__state = 0;
Label_024B:
<4>t__$awaitb = this.<a4>t__$awaitc.GetResult();
this.<a4>t__$awaitc = new TaskAwaiter<string>();
this.<>g__initLocal0 = new CustomData();
this.<>g__initLocal0.Id = <3>t__$await9;
this.<>g__initLocal0.Description = <4>t__$awaitb;
this.<data2>5__4 = this.<>g__initLocal0;
Console.WriteLine("\nObject initializer ===============");
Console.WriteLine("Id: {0}", this.<data2>5__4.Id);
Console.WriteLine("Description: {0}", this.<data2>5__4.Description);
}
catch (Exception <>t__ex)
{
this.<>1__state = -1;
this.$builder.SetException(<>t__ex);
return;
}
this.<>1__state = -1;
this.$builder.SetResult();
}
Обратите внимание, что результаты асинхронных вызовов методов класса CustomDataProviderAsync записываются в локальные переменные.
При использовании стандартного вариант инициализации свойств в блоках за метками Label_00B7 и Label_012E осуществляется асинхронный запрос данных. Результаты сразу записываются в экземпляр класса CustomData. По одному свойству в каждом блоке.
А вот при использовании инициализатора происходит установка значений всех свойств в одном блоке. Дело в том, что он разворачивается компилятором в создание промежуточного объекта, который затем инициализируется стандартным способом (после Label_024B). Поэтому, запись результата в переменную <3>t__$await9 и передача его в свойство Id расположены в разных блоках. При этом, выполнение каждого из них происходит при отдельных обращениях к методу. Это означает, что в момент выполнения:
this.<>g__initLocal0.Id = <3>t__$await9
переменная <3>t__$await9 будет иметь значение по умолчанию. Т.е. тот самый 0, который и был получен. Как легко понять, в данном случае реальное значение будет передано только в последнее из списка инициализируемых свойств.
Остается надеяться, что данная ошибка будет исправлена до выхода финальной версии C# 5.0, в который будет включать все возможности Async CTP.