IDisposable.Dispose() не вызывается в режиме Release для асинхронного метода

Я написал следующее примерное приложение WPF в VB.NET 14, используя .NET 4.6.1 на VS2015.1:

Class MainWindow

    Public Sub New()
        InitializeComponent()
    End Sub

    Private Async Sub Button_Click(sender As Object, e As RoutedEventArgs)
        MessageBox.Show("Pre")

        Using window = New DisposableWindow()
            window.Show()

            For index = 1 To 1
                Await Task.Delay(100)
            Next
        End Using

        MessageBox.Show("Post")
    End Sub

    Class DisposableWindow
        Inherits Window
        Implements IDisposable

        Public Sub Dispose() Implements IDisposable.Dispose
            Me.Close()
            MessageBox.Show("Disposed")
        End Sub
    End Class

End Class

Пример ниже дает следующий результат:

  • Режим отладки: Pre, Disposed, Post
  • Режим выпуска: Pre, Post

Это странно. Почему режим Debug выполняет этот код иначе, чем режим Release...?

Когда я меняю блок использования на ручной блок try/finally, вызов в window.Dispose() даже выдает исключение NullReferenceException:

Dim window = New DisposableWindow()
Try
    window.Show()

    For index = 1 To 1
        Await Task.Delay(100)
    Next
Finally
    window.Dispose()
End Try

И еще более странный материал: когда исключение for-loop исключено, образец работает отлично. Я разрешаю только цикл For-loop, чтобы указать минимальное количество циклов, которые создают проблему. Также не стесняйтесь заменять For-loop на цикл While. Он производит то же поведение, что и For-loop.

Работы:

Using window = New DisposableWindow()
    window.Show()

    Await Task.Delay(100)
End Using

Теперь вы можете подумать: "Это странно!". Это становится еще хуже. Я также сделал тот же пример в С# (6), где он отлично работает. Таким образом, в С# оба режима Debug и Release приводят к выводу "Pre, Disposed, Post" в качестве вывода.

Сэмплы можно скачать здесь:

http://www.filedropper.com/vbsample

http://www.filedropper.com/cssample

На данный момент я очень взволнован. Является ли это ошибкой в ​​стеке VB.NET.NET Framework? Или я пытаюсь выполнить что-то странное, что по счастью кажется работой на С# и частично в VB.NET?

Edit:

Проделал еще несколько тестов:

  • Отключение оптимизаций компилятора в режиме VB.NET для выпуска, приводит к тому, что он ведет себя как режим отладки (как и ожидалось, но хотел проверить его на всякий случай).
  • Проблема также возникает, когда я нацелен на .NET 4.5 (самая ранняя версия, где async/await стал доступен).

Update:

С тех пор исправлено. Публичный релиз запланирован на версию 1.2, но последняя версия в главной ветке должна содержать исправление.

Смотрите: https://github.com/dotnet/roslyn/issues/7669

Ответ 1

Я напишу об этом, эта ошибка Roslyn чрезвычайно неприятна и может сломать множество программ VB.NET. В очень уродливой и трудно диагностируемой форме.

Ошибка довольно трудно увидеть, вам нужно посмотреть на сгенерированную сборку с декомпилятором. Я опишу его на скорости разлома. Операторы в Async Sub переписываются в конечный автомат, а конкретное имя класса в вашем фрагменте - VB $StateMachine_1_buttonClick. Вы можете видеть его только с достойным декомпилятором. Метод MoveNext() этого класса выполняет инструкции в теле метода. Этот метод вводится несколько раз, пока выполняется ваш асинхронный код.

Переменные, используемые MoveNext(), необходимо захватить, превратив ваши локальные переменные в поля класса. Как и ваша переменная window, она понадобится позже, когда заканчивается оператор Using, и нужно вызвать метод Dispose(). Имя этой переменной в сборке Debug равно $VB$ResumableLocal_window$0. Когда вы создаете сборку Release своей программы, компилятор пытается оптимизировать этот класс и плохо работает. Он устраняет захват и делает window локальной переменной MoveNext(). Это ужасно неправильно, когда выполнение возобновляется после Await, эта переменная будет Nothing. И поэтому его метод Dispose() не будет вызываться.

Эта ошибка Roslyn имеет очень большой эффект afaict, она сломает любой код VB.NET, который использует оператор Using в методе Async, где тело оператора содержит Await. Это нелегко диагностировать, отсутствующий вызов Dispose() очень часто остается незамеченным. За исключением случая, подобного вашему, где он имеет очень видимый побочный эффект. Должно быть много запущенных программ, которые имеют эту ошибку прямо сейчас. Побочным эффектом является то, что они будут работать "тяжелыми", потребляя больше ресурсов, чем необходимо. Программа может терпеть неудачу во многих трудных целях диагностики.

Существует временный обходной путь для этой ошибки, не забудьте никогда не развертывать сборку Debug вашего приложения VB.NET, у которого есть другие проблемы. Вместо этого выключите оптимизатор. Выберите сборку выпуска и используйте "Проект" > "Свойства" > вкладка "Компиляция" > "Дополнительные параметры компиляции" > установите флажок "Включить оптимизацию".

Yikes, это плохо.