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

时间:2011-01-09 23:58:24

标签: .net struct closures mutable ienumerator

更新:嗯,现在我已经离开并完成了它:我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() { }
}

1。在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

进一步评论

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

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

4 个答案:

答案 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