为什么在using语句中的闭包内捕获可变结构变量会改变其本地行为?

Dan*_*Tao 20 .net ienumerator struct closures mutable

更新:好吧,现在我已经完成了它:我向微软提交了一个关于此的错误报告,因为我严重怀疑这是正确的行为.那就是说,我仍然不能100%肯定对这个问题有什么看法; 所以我可以看到什么是"正确的"是开放的某种程度的解释.

我的感觉是,微软会接受这是一个错误,或者回应一个using语句中的可变值类型变量的修改构成未定义的行为.

此外,对于它的价值,我至少猜测这里发生了什么.我怀疑编译器正在为闭包生成一个类,将局部变量"提升"到该类的实例字段; 因为它在一个using街区内,所以它正在建造这个领域readonly.正如LukeH在对另一个问题的评论中指出的那样,这会阻止方法调用,例如MoveNext修改字段本身(它们会影响副本).


注意:我已经缩短了这个问题的可读性,尽管它仍然不是很短.有关完整的原始(较长)问题,请参阅编辑历史记录.

我已经阅读了我认为是ECMA-334相关章节的内容,似乎无法找到这个问题的结论性答案.我将首先说明问题,然后为感兴趣的人提供一些附加评论的链接.

如果我有一个可实现的可变值类型IDisposable,我可以(1)调用一个方法来修改using语句中局部变量值的状态,并且代码的行为与我期望的一样.但是,一旦我在语句中的闭包捕获了有问题的变量using,(2)在本地范围内不再可以看到对值的修改.

只有在闭包内using语句中捕获变量的情况下,此行为才会显现; 当只有一个(using)或其他条件(闭包)存在时,这是不明显的.

为什么在using语句中的闭包内捕获可变值类型的变量会改变其本地行为?

下面是说明第1项和第2项的代码示例.两个示例都将使用以下演示Mutable值类型:

struct Mutable : IDisposable
{
    int _value;
    public int Increment()
    {
        return _value++;
    }

    public void Dispose() { }
}
Run Code Online (Sandbox Code Playgroud)

1.在using块中变换值类型变量

using (var x = new Mutable())
{
    Console.WriteLine(x.Increment());
    Console.WriteLine(x.Increment());
}
Run Code Online (Sandbox Code Playgroud)

输出代码输出:

0
1

2.在using块内的闭包内捕获值类型变量

using (var x = new Mutable())
{
    // x is captured inside a closure.
    Func<int> closure = () => x.Increment();

    // Now the Increment method does not appear to affect the value
    // of local variable x.
    Console.WriteLine(x.Increment());
    Console.WriteLine(x.Increment());
}
Run Code Online (Sandbox Code Playgroud)

以上代码输出:

0
0

进一步评论

已经注意到Mono编译器提供了我期望的行为(在using+闭包情况下仍然可以看到局部变量值的变化).我不清楚这种行为是否正确.

有关此问题的更多想法,请参阅此处.

cdh*_*wie 11

这与生成和使用闭包类型的方式有关.csc使用这些类型的方式似乎有一个微妙的错误.例如,以下是调用MoveNext()时由Mono的gmcs生成的IL:

      IL_0051:  ldloc.3
      IL_0052:  ldflda valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> Foo/'<Main>c__AnonStorey0'::enumerator
      IL_0057:  call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
Run Code Online (Sandbox Code Playgroud)

请注意,它正在加载字段的地址,这允许方法调用修改存储在闭包对象上的值类型的实例.这是我认为是正确的行为,这导致列表内容被枚举得很好.

以下是csc生成的内容:

      IL_0068:  ldloc.3
      IL_0069:  ldfld valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> Tinker.Form1/'<>c__DisplayClass3'::enumerator
      IL_006e:  stloc.s 5
      IL_0070:  ldloca.s 5
      IL_0072:  call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()
Run Code Online (Sandbox Code Playgroud)

所以在这种情况下,它会获取值类型实例的副本并在副本上调用该方法.毫无疑问,为什么这会让你无处可去.get_Current()调用同样错误:

      IL_0052:  ldloc.3
      IL_0053:  ldfld valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> Tinker.Form1/'<>c__DisplayClass3'::enumerator
      IL_0058:  stloc.s 5
      IL_005a:  ldloca.s 5
      IL_005c:  call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
      IL_0061:  call void class [mscorlib]System.Console::WriteLine(int32)
Run Code Online (Sandbox Code Playgroud)

由于枚举器的状态,它的复制没有调用MoveNext(),get_Current()显然会返回default(int).

简而言之:csc似乎是错误的.有趣的是Mono得到了这个权利,而MS.NET却没有!

...我很想听到Jon Skeet对这种特殊怪异的评论.


在与#mono中的brajkovic的讨论中,他确定C#语言规范实际上并没有详细说明应该如何实现闭包类型,也不确定如何转换在闭包中捕获的locals的访问.规范中的示例实现似乎使用csc使用的"复制"方法.因此,根据语言规范,编译器输出可以被认为是正确的,尽管我认为csc应该至少在方法调用之后将本地复制回闭包对象.


Eri*_*ert 7

这是一个已知的错误; 几年前我们发现了它.修复可能会破坏,问题非常模糊; 这些是反对修复它的要点.因此,它从来没有被优先考虑到足以实际修复它.

这已经在我的潜在博客主题的队列中存在了几年了; 也许我应该写出来.

顺便说一下,你对解释这个bug的机制的推测是完全准确的; 那里很好的通灵调试.

所以,是的,已知的bug,但感谢您的报告,无论如何!