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)
using块中变换值类型变量using (var x = new Mutable())
{
Console.WriteLine(x.Increment());
Console.WriteLine(x.Increment());
}
Run Code Online (Sandbox Code Playgroud)
输出代码输出:
0 1
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应该至少在方法调用之后将本地复制回闭包对象.
这是一个已知的错误; 几年前我们发现了它.修复可能会破坏,问题非常模糊; 这些是反对修复它的要点.因此,它从来没有被优先考虑到足以实际修复它.
这已经在我的潜在博客主题的队列中存在了几年了; 也许我应该写出来.
顺便说一下,你对解释这个bug的机制的推测是完全准确的; 那里很好的通灵调试.
所以,是的,已知的bug,但感谢您的报告,无论如何!
| 归档时间: |
|
| 查看次数: |
1047 次 |
| 最近记录: |