20

更新:好吧,现在我已经完成了:我向微软提交了一份错误报告,因为我严重怀疑这是正确的行为。也就是说,对于这个问题,我仍然不能 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() { }
}

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

using (var x = new Mutable())
{
    Console.WriteLine(x.Increment());
    Console.WriteLine(x.Increment());
}

输出代码输出:

0
1

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

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+ 闭包情况下仍然可见)。我不清楚这种行为是否正确。

有关我对这个问题的更多想法,请参见此处

4

4 回答 4

11

这与生成和使用闭包类型的方式有关。csc 使用这些类型的方式似乎存在一个微妙的错误。例如,这里是 Mono 的 gmcs 在调用 MoveNext() 时生成的 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 至少应该在方法调用之后将本地复制回闭包对象。

于 2011-01-10T00:11:11.323 回答
7

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

几年来,这一直在我的潜在博客主题队列中;也许我应该把它写下来。

顺便说一句,您对解释该错误的机制的猜想是完全准确的;那里很好的心理调试。

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

于 2011-01-14T06:56:20.143 回答
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

于 2011-01-10T00:12:30.480 回答
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
于 2011-01-10T00:15:46.543 回答