Инициализатор объекта + await = ошибка

Иногда случаются ситуации, когда ошибки появляются, как говорится, на ровном месте. Давайте рассмотрим один из таких случаев. А именно, когда использование инициализатора объекта приводит к некорректному поведению программы.

Например, код с подобной ошибкой можно найти в описании возможностей 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.

Добавить комментарий