VB.NET 迭代器函数丢失局部变量

VB.NET Iterator Function Loses Local Variables

couple showstoppers 延迟迁移到 .NET 4.6 之后 运行 我终于适应了迁移到 C#6/VB14 编译器,直到我遇到一个关键问题VB.NET 中的迭代器函数会丢弃局部变量。

以下代码示例在 Visual Studio 2015 / msbuild 中以发布模式(优化)编译时,将在注释行上抛出空引用异常。

Module Module1

    Sub Main()
        For Each o As Integer In GetAllStuff()
            Console.WriteLine(o.ToString())
        Next

        Console.ReadKey()

    End Sub

    Private Iterator Function GetAllStuff() As IEnumerable(Of Integer)
        Dim map As Dictionary(Of String, String) = New Dictionary(Of String, String)
        Dim tasks As New List(Of Integer)
        tasks.Add(1)

        For Each task As Integer In tasks
            Yield task
        Next

        'The value of map becomes null here under the new VB14 compiler in Release on .NET 4.6'
        For Each s As String In map.Values
            Yield 100
        Next
    End Function

End Module

所以,这很可怕。

值得注意的是,这段代码的 C# 等价物可以毫无问题地执行。更重要的是,这在以前版本的 VB 编译器下有效(并且已经有效)。比较两个不同编译器创建的状态机之间的 MSIL,新编译器似乎几乎完全使用 .locals 进行局部变量存储,而旧编译器使用状态机上的可变字段来保存局部值。

我错过了什么吗?我无法在 VB 中找到任何关于迭代器重大更改的文档(我也无法想象会是这种情况),但也没有发现其他人遇到过这个问题。

可以通过将 map 的构造移动到第一个 foreach 循环之后来解决这个特殊示例,但是我担心的是我对这个问题的真正含义没有任何感觉。我对将代码修改为 "just make it work." 不感兴趣,在我们广泛的代码库中还有什么地方我可以 运行 遇到同样的问题?我已经在 Connect 上提交了这个问题,但它常常让人感觉像是一个黑洞。

更新

有人刚刚在 Roslyn GitHub 页面上报告了与异步状态机相同的问题:https://github.com/dotnet/roslyn/issues/9001

希望这开始得到一点关注。

首先,感谢您关注我向 Roslyn 团队提出的问题。

我从 https://github.com/dotnet/roslyn(master 分支)中提取了最新的 Roslyn 源代码,并向 BasicCompilerEmitTest 项目添加了一个额外的单元测试,如下所示:

Imports Microsoft.CodeAnalysis.VisualBasic.UnitTests

Public Class KirillsTests
  Inherits BasicTestBase

  <Fact>
  Public Sub IteratorVariableCaptureTest()
    Dim source =
<compilation name="Iterators">
  <file name="a.vb">
Imports System
Imports System.Collections.Generic

Module Module1

    Sub Main()
        For Each o As Integer In GetAllStuff()
            Console.WriteLine(o.ToString())
        Next

        Console.WriteLine("done")
    End Sub

    Private Iterator Function GetAllStuff() As IEnumerable(Of Integer)
        Dim map As Dictionary(Of String, String) = New Dictionary(Of String, String)
        Dim tasks As New List(Of Integer)
        tasks.Add(1)

        For Each task As Integer In tasks
            Yield task
        Next

        'The value of map becomes null here under the new VB14 compiler in Release on .NET 4.6'
        For Each s As String In map.Values
            Yield 100
        Next
    End Function

End Module
  </file>
</compilation>

    Dim expectedOutput = <![CDATA[1
done]]>

    Dim compilation = CompilationUtils.CreateCompilationWithReferences(source, references:=LatestVbReferences, options:=TestOptions.DebugExe)
    CompileAndVerify(compilation, expectedOutput:=expectedOutput)
    CompileAndVerify(compilation.WithOptions(TestOptions.ReleaseExe), expectedOutput:=expectedOutput)
  End Sub

End Class

由于 XElementXCData 的使用,这看起来像是一团乱麻,但这是其他 Roslyn 单元测试使用的格式。

我只对您在问题中发布的代码做了一处改动——即将 Console.ReadKey() 替换为 Console.WriteLine("done") 以便我可以跟踪成功完成(因为 CompileAndVerify 只是忽略异常).

以上测试通过。 map.Values访问上没有NullReferenceException,输出为:

1
done

...正如预期的那样。因此,您的错误似乎已得到修复 - 尽管我无法判断该修复是否会随 Visual Studio 2015 Update 2 一起提供。

async variable capture issue was fixed by pull request #7693,但 DataFlowPass.SetSlotUnassigned 已被重写(拆分为 2 个方法并修改)所以我无法确认您发现的迭代器问题是否已由该特定拉取请求修复,或者由其他一些代码更改。