更新:嗯,现在我已经离开并完成了它:我filed a bug report with Microsoft关于此,因为我严重怀疑这是正确的行为。也就是说,我仍然不能100%确定对this question有什么看法;所以我可以看到什么是“正确的”是一些级别的解释。
我的感觉是微软会接受这是一个错误,或者回应一个using
语句中的可变值类型变量的修改构成了未定义的行为。
另外,对于它的价值,我至少有猜测这里发生了什么。我怀疑编译器正在为闭包生成一个类,将局部变量“提升”到该类的实例字段;由于它位于using
块内,正在使字段readonly
。正如LukeH在a comment to the other question中指出的那样,这会阻止诸如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() { }
}
using
块using (var x = new Mutable())
{
Console.WriteLine(x.Increment());
Console.WriteLine(x.Increment());
}
输出代码输出:
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());
}
以上代码输出:
0 0
已经注意到Mono编译器提供了我期望的行为(局部变量值的变化在using
+闭包情况下仍然可见)。我不清楚这种行为是否正确。
有关此问题的更多想法,请参阅here。
答案 0 :(得分: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()
请注意,它正在加载字段的地址,这允许方法调用修改存储在闭包对象上的值类型的实例。这是我认为是正确的行为,这导致列表内容被枚举得很好。
以下是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()
所以在这种情况下,它会获取值类型实例的副本并在副本上调用该方法。毫无疑问,为什么这会让你无处可去。 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)
由于它复制的枚举器的状态没有调用MoveNext(),get_Current()显然会返回default(int)
。
简而言之:csc似乎是错误的。有趣的是Mono在MS.NET没有这样做的时候做到了这一点!
...我很想听到Jon Skeet对这种特殊怪异的评论。
在与#mono中的brajkovic的讨论中,他确定C#语言规范实际上没有详细说明应该如何实现闭包类型 ,也不确定如何在闭包中捕获的本地人的访问得到翻译。规范中的示例实现似乎使用csc使用的“复制”方法。因此,根据语言规范,编译器输出可以被认为是正确的,但我认为csc至少应该在方法调用之后将本地复制回闭包对象。
答案 1 :(得分:7)
这是一个已知的错误;几年前我们发现了它。修复可能会破坏,问题非常模糊;这些是反对修复它的要点。因此,它从未被优先考虑到足以实际修复它。
这已经在我的潜在博客主题排队了几年了;也许我应该写出来。
顺便说一句,你对解释这个bug的机制的推测是完全准确的;很好的通灵调试。
所以,是的,已知错误,但感谢报告,无论如何!
答案 2 :(得分:0)
编辑 - 这是不正确的,我没有仔细阅读这个问题。
将结构放入闭包会导致赋值。值类型的赋值会生成该类型的副本。所以正在发生的是你正在创建一个新的Enumerator<int>
,并且该枚举器上的Current
将返回0。
using System;
using System.Collections.Generic;
class Program
{
static void Main(string[] args)
{
List<int> l = new List<int>();
Console.WriteLine(l.GetEnumerator().Current);
}
}
结果:0
答案 3 :(得分:0)
问题是枚举器存储在另一个类中,因此每个操作都使用枚举器的副本。
[CompilerGenerated]
private sealed class <>c__DisplayClass3
{
// Fields
public List<int>.Enumerator enumerator;
// Methods
public int <Main>b__1()
{
return this.enumerator.Current;
}
}
public static void Main(string[] args)
{
List<int> <>g__initLocal0 = new List<int>();
<>g__initLocal0.Add(1);
<>g__initLocal0.Add(2);
<>g__initLocal0.Add(3);
List<int> list = <>g__initLocal0;
Func<int> CS$<>9__CachedAnonymousMethodDelegate2 = null;
<>c__DisplayClass3 CS$<>8__locals4 = new <>c__DisplayClass3();
CS$<>8__locals4.enumerator = list.GetEnumerator();
try
{
if (CS$<>9__CachedAnonymousMethodDelegate2 == null)
{
CS$<>9__CachedAnonymousMethodDelegate2 = new Func<int>(CS$<>8__locals4.<Main>b__1);
}
while (CS$<>8__locals4.enumerator.MoveNext())
{
Console.WriteLine(CS$<>8__locals4.enumerator.Current);
}
}
finally
{
CS$<>8__locals4.enumerator.Dispose();
}
}
没有lambda,代码就更接近你期望的了。
public static void Main(string[] args)
{
List<int> <>g__initLocal0 = new List<int>();
<>g__initLocal0.Add(1);
<>g__initLocal0.Add(2);
<>g__initLocal0.Add(3);
List<int> list = <>g__initLocal0;
using (List<int>.Enumerator enumerator = list.GetEnumerator())
{
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
}
}
}
特定IL
L_0058: ldfld valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> Machete.Runtime.Environment/<>c__DisplayClass3::enumerator
L_005d: stloc.s CS$0$0001
L_005f: ldloca.s CS$0$0001