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

发布于 2024-10-11 07:30:27 字数 2453 浏览 7 评论 0原文

更新:好吧,现在我已经完成了:我 向 Microsoft 提交了有关此问题的错误报告,因为我严重怀疑这是正确的行为。也就是说,对于这个问题,我仍然不是 100% 确定该相信什么;所以我可以看到什么是“正确的”可以对某种程度的解释开放。

我的感觉是,微软要么会接受这是一个错误,要么会回应在 using 语句中修改可变值类型变量构成未定义的行为。

另外,无论如何,我至少对这里发生的事情有一个猜测。我怀疑编译器正在为闭包生成一个类,将局部变量“提升”到该类的实例字段;由于它位于 using 块内,因此它使该字段成为只读。正如 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

2. 在 using 块内捕获闭包内的值类型变量code>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 中仍然可见) + 封闭案例)。我不清楚这种行为是否正确。

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

Update: Well, now I've gone and done it: I filed a bug report with Microsoft about this, as I seriously doubt that it is correct behavior. That said, I'm still not 100% sure what to believe regarding this question; so I can see that what is "correct" is open to some level of interpretation.

My feeling is that either Microsoft will accept that this is a bug, or else respond that the modification of a mutable value type variable within a using statement constitutes undefined behavior.

Also, for what it's worth, I have at least a guess as to what is happening here. I suspect that the compiler is generating a class for the closure, "lifting" the local variable to an instance field of that class; and since it is within a using block, it's making the field readonly. As LukeH pointed out in a comment to the other question, this would prevent method calls such as MoveNext from modifying the field itself (they would instead affect a copy).


Note: I have shortened this question for readability, though it is still not exactly short. For the original (longer) question in its entirety, see the edit history.

I have read through what I believe are the relevant sections of the ECMA-334 and cannot seem to find a conclusive answer to this question. I will state the question first, then provide a link to some additional comments for those who are interested.

Question

If I have a mutable value type that implements IDisposable, I can (1) call a method that modifies the state of the local variable's value within a using statement and the code behaves as I expect. Once I capture the variable in question inside a closure within the using statement, however, (2) modifications to the value are no longer visible in the local scope.

This behavior is only apparent in the case where the variable is captured inside the closure and within a using statement; it is not apparent when only one (using) or the other condition (closure) is present.

Why does capturing a variable of a mutable value type inside a closure within a using statement change its local behavior?

Below are code examples illustrating items 1 and 2. Both examples will utilize the following demonstration Mutable value type:

struct Mutable : IDisposable
{
    int _value;
    public int Increment()
    {
        return _value++;
    }

    public void Dispose() { }
}

1. Mutating a value type variable within a using block

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

The output code outputs:

0
1

2. Capturing a value type variable inside a closure within a using block

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());
}

The above code outputs:

0
0

Further Comments

It has been noted that the Mono compiler provides the behavior I expect (changes to the value of the local variable are still visible in the using + closure case). Whether this behavior is correct or not is unclear to me.

For some more of my thoughts on this issue, see here.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

梦纸 2024-10-18 07:30:27

这与闭包类型的生成和使用方式有关。 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 至少应该在方法调用之后将本地复制回闭包对象。

This has to do with the way closure types are generated and used. There appears to be a subtle bug in the way csc uses these types. For example, here is the IL generated by Mono's gmcs when invoking 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()

Note that it's loading the field's address, which allows the method call to modify the instance of the value type stored on the closure object. This is what I would consider to be correct behavior, and this results in the list contents being enumerated just fine.

Here's what csc generates:

      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()

So in this case it's taking a copy of the value type instance and invoking the method on the copy. It should be no surprise why this gets you nowhere. The get_Current() call is similarly wrong:

      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)

Since the state of the enumerator it's copying has not had MoveNext() called, get_Current() apparently returns default(int).

In short: csc appears to be buggy. It's interesting that Mono got this right while MS.NET did not!

...I'd love to hear Jon Skeet's comments on this particular oddity.


In a discussion with brajkovic in #mono, he determined that the C# language specification does not actually detail how the closure type should be implemented, nor how accesses of locals that are captured in the closure should get translated. An example implementation in the spec seems to use the "copy" method that csc uses. Therefore either compiler output can be considered correct according to the language specification, though I would argue that csc should at least copy the local back to the closure object after the method call.

相思碎 2024-10-18 07:30:27

这是一个已知的错误;我们几年前就发现了它。该修复可能会被破坏,而且问题相当模糊;这些都是反对修复它的观点。因此,它的优先级从来没有足够高来实际修复它。

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

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

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

It's a known bug; we discovered it a couple years ago. The fix would be potentially breaking, and the problem is pretty obscure; these are points against fixing it. Therefore it has never been prioritized high enough to actually fix it.

This has been in my queue of potential blog topics for a couple years now; perhaps I ought to write it up.

And incidentally, your conjecture as to the mechanism that explains the bug is completely accurate; nice psychic debugging there.

So, yes, known bug, but thanks for the report regardless!

幸福还没到 2024-10-18 07:30:27

编辑 - 这是不正确的,我没有足够仔细地阅读问题。

将结构放入闭包中会导致分配。对值类型的赋值会生成该类型的副本。因此,您正在创建一个新的 Enumerator,并且该枚举器上的 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

EDIT - This is incorrect, I didn't read the question carefully enough.

Placing the struct into a closure causes an assignment. Assignments on value types result in a copy of the type. So what's happening is you are creating a new Enumerator<int>, and Current on that enumerator will return 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);
    }
}

Result: 0

远昼 2024-10-18 07:30:27

问题是枚举数存储在另一个类中,因此每个操作都使用枚举数的副本。

[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
lt;>9__CachedAnonymousMethodDelegate2 = null;
    <>c__DisplayClass3 CS
lt;>8__locals4 = new <>c__DisplayClass3();
    CS
lt;>8__locals4.enumerator = list.GetEnumerator();
    try
    {
        if (CS
lt;>9__CachedAnonymousMethodDelegate2 == null)
        {
            CS
lt;>9__CachedAnonymousMethodDelegate2 = new Func<int>(CS
lt;>8__locals4.<Main>b__1);
        }
        while (CS
lt;>8__locals4.enumerator.MoveNext())
        {
            Console.WriteLine(CS
lt;>8__locals4.enumerator.Current);
        }
    }
    finally
    {
        CS
lt;>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

The problem is the enumerator is stored in another class so every action is working with a copy of the enumerator.

[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
lt;>9__CachedAnonymousMethodDelegate2 = null;
    <>c__DisplayClass3 CS
lt;>8__locals4 = new <>c__DisplayClass3();
    CS
lt;>8__locals4.enumerator = list.GetEnumerator();
    try
    {
        if (CS
lt;>9__CachedAnonymousMethodDelegate2 == null)
        {
            CS
lt;>9__CachedAnonymousMethodDelegate2 = new Func<int>(CS
lt;>8__locals4.<Main>b__1);
        }
        while (CS
lt;>8__locals4.enumerator.MoveNext())
        {
            Console.WriteLine(CS
lt;>8__locals4.enumerator.Current);
        }
    }
    finally
    {
        CS
lt;>8__locals4.enumerator.Dispose();
    }
}

Without the lambda the code is closer to what you would expect.

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);
        }
    }
}

Specific 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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文