为什么在使用语句中捕获闭合内部的可变结构变量会改变其本地行为?
-
09-10-2019 - |
题
更新: :嗯,现在我去了:我 用Microsoft提交了错误报告 关于这一点,我严重怀疑这是正确的行为。也就是说,我仍然不是100%确定要相信什么 这个问题;因此,我可以看到“正确”的内容是开放的 一些 解释水平。
我的感觉是Microsoft会接受这是一个错误,或者回答说对一个可变值类型变量的修改 using
声明构成不确定的行为。
另外,就价值而言,我至少有一个 猜测 至于这里发生了什么。我怀疑编译器正在生成一个用于关闭的类,将局部变量“抬高”到该类的实例字段;而且由于它在 using
堵塞, 它正在做这个领域 readonly
. 。正如卢克指出的那样 对另一个问题的评论, ,这将防止诸如 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() { }
}
1.在A内突变值类型变量 using
堵塞
using (var x = new Mutable())
{
Console.WriteLine(x.Increment());
Console.WriteLine(x.Increment());
}
输出代码输出:
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());
}
以上代码输出:
0 0
进一步的评论
已经注意到,单声道编译器提供了我期望的行为(在本地变量的价值的变化中仍然可见 using
+关闭案例)。这种行为是否正确,我不清楚。
有关我对这个问题的更多想法,请参阅 这里.
解决方案
这是一个已知的错误;几年前,我们发现了它。解决方案可能会破裂,问题非常晦涩。这些是不适合修复它的要点。因此,它从来没有得到足够高的优先级来实际修复它。
这已经在我的潜在博客主题队列中已经有几年了。也许我应该把它写成。
顺便说一下,您对解释该错误的机制的猜想是完全准确的;在那里不错的心理调试。
所以,是的,已知的错误,但感谢您的报告!
其他提示
这与生成和使用闭合类型的方式有关。 CSC使用这些类型的方式似乎有一个微妙的错误。例如,这是单声道GMC生成的IL,当调用moveNext():
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)
由于枚举的状态已复制,因此没有compeNext()呼叫get_current()显然返回 default(int)
.
简而言之:CSC似乎是越野车。有趣的是,Mono在Net女士没有的情况下做到了这一点!
...我很想听听乔恩·斯基特(Jon Skeet)对这种特殊奇怪的评论。
在与#Nono的Brajkovic的讨论中,他确定C#语言规范实际上并未详细介绍 如何 应实现封闭类型,也应如何在关闭中捕获的当地人的访问。规格中的实现示例实现似乎使用了CSC使用的“复制”方法。因此,根据语言规范,可以将任一编译器输出视为正确,尽管我认为CSC至少应在方法调用后将本地返回重新返回到闭合对象。
编辑 - 这是不正确的,我没有仔细阅读这个问题。
将结构置于封闭中会导致分配。价值类型的作业导致类型的副本。所以发生的事情是您正在创建一个新的 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
问题是枚举者存储在另一类中,因此每个动作都与枚举者的副本一起使用。
[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