这个对象生命周期扩展闭包是C#编译器的一个bug吗?

135
我正在回答一个与闭包可能(合法地)延长对象生命周期有关的问题,然后遇到了 C# 编译器(如果有影响则为 4.0 版本)的一些极其好奇的代码生成。
我能找到的最简复现代码如下:
1.创建一个 lambda 表达式,该表达式在调用包含类型的静态方法时捕获一个局部变量。 2.将生成的委托引用分配给包含对象的实例字段。
结果: 编译器创建了一个闭包对象,该闭包对象引用了创建 lambda 表达式的对象,尽管没有理由这么做 - 委托的"内部"目标是一个静态方法,并且在执行委托时不需要(也不会)访问 lambda 创建对象的实例成员。实际上,编译器的行为就像程序员无缘无故地捕获了this
class Foo
{
    private Action _field;

    public void InstanceMethod()
    {
        var capturedVariable = Math.Pow(42, 1);

        _field = () => StaticMethod(capturedVariable);
    }

    private static void StaticMethod(double arg) { }
}

生成的发布版本代码(反编译为“简化”的C#)如下所示:

public void InstanceMethod()
{

    <>c__DisplayClass1 CS$<>8__locals2 = new <>c__DisplayClass1();

    CS$<>8__locals2.<>4__this = this; // What's this doing here?

    CS$<>8__locals2.capturedVariable = Math.Pow(42.0, 1.0);
    this._field = new Action(CS$<>8__locals2.<InstanceMethod>b__0);
}

[CompilerGenerated]
private sealed class <>c__DisplayClass1
{
    // Fields
    public Foo <>4__this; // Never read, only written to.
    public double capturedVariable;

    // Methods
    public void <InstanceMethod>b__0()
    {
        Foo.StaticMethod(this.capturedVariable);
    }
}

请注意,闭包对象的<>4__this字段被填充了一个对象引用,但从未被读取(没有理由)。
那么这是怎么回事?语言规范允许吗?这是编译器的错误/怪异行为还是有一个良好的原因(我显然错过了),使闭包引用该对象?这让我感到焦虑,因为这看起来像是一个闭包程序员(像我一样)无意中会在程序中引入奇怪的内存泄漏(想象一下如果委托被用作事件处理程序)。

19
有趣。在我看来这似乎是个 bug。请注意,如果您没有给实例字段赋值(例如,如果您返回该值),它不会捕获'this'。 - Jon Skeet
15
我无法在VS11开发者预览版中重现此问题。但可以在VS2010SP1中进行重现。看起来已经解决了 :) - leppie
2
这也发生在VS2008SP1中。对于VS2010SP1,无论是3.5还是4.0都会发生。 - leppie
5
“bug”这个词用于描述这种情况有点太严重了。编译器只是生成了稍微低效一些的代码,肯定不是内存泄漏,因为垃圾回收可以正常运作。可能在处理异步实现时已经修复了这个问题。 - Hans Passant
7
@Hans,如果委托对象的生命周期比主对象长,那么这将无法进行垃圾回收,并且没有任何措施可以防止这种情况发生。请翻译以上内容。 - SoftMemes
显示剩余9条评论
2个回答

24

看起来像是一个bug。感谢您提醒我,我会调查一下。有可能这个问题已经被发现并解决了。


7

这似乎是一个错误或不必要的问题:

我在IL语言中运行了您的示例:

.method public hidebysig 
    instance void InstanceMethod () cil managed 
{
    // Method begins at RVA 0x2074
    // Code size 63 (0x3f)
    .maxstack 4
    .locals init (
        [0] class ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'   'CS$<>8__locals2'
    )

    IL_0000: newobj instance void ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::.ctor()
    IL_0005: stloc.0
    IL_0006: ldloc.0
    IL_0007: ldarg.0
    IL_0008: stfld class ConsoleApplication1.Program/Foo ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::'<>4__this' //Make ref to this
    IL_000d: nop
    IL_000e: ldloc.0
    IL_000f: ldc.r8 42
    IL_0018: ldc.r8 1
    IL_0021: call float64 [mscorlib]System.Math::Pow(float64, float64)
    IL_0026: stfld float64 ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::capturedVariable
    IL_002b: ldarg.0
    IL_002c: ldloc.0
    IL_002d: ldftn instance void ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::'<InstanceMethod>b__0'()
    IL_0033: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
    IL_0038: stfld class [mscorlib]System.Action ConsoleApplication1.Program/Foo::_field
    IL_003d: nop
    IL_003e: ret
} // end of method Foo::InstanceMethod

例子2:

class Program
{
    static void Main(string[] args)
    {
    }


    class Foo
    {
        private Action _field;

        public void InstanceMethod()
        {
            var capturedVariable = Math.Pow(42, 1);

            _field = () => Foo2.StaticMethod(capturedVariable);  //Foo2

        }

        private static void StaticMethod(double arg) { }
    }

    class Foo2
    {

        internal static void StaticMethod(double arg) { }
    }


}

在cl中:(注意!!现在this引用已经消失!)

public hidebysig 
        instance void InstanceMethod () cil managed 
    {
        // Method begins at RVA 0x2074
        // Code size 56 (0x38)
        .maxstack 4
        .locals init (
            [0] class ConsoleApplication1.Program/Foo/'<>c__DisplayClass1' 'CS$<>8__locals2'
        )

        IL_0000: newobj instance void ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::.ctor()
        IL_0005: stloc.0
        IL_0006: nop //No this pointer
        IL_0007: ldloc.0
        IL_0008: ldc.r8 42
        IL_0011: ldc.r8 1
        IL_001a: call float64 [mscorlib]System.Math::Pow(float64, float64)
        IL_001f: stfld float64 ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::capturedVariable
        IL_0024: ldarg.0 //No This ref
        IL_0025: ldloc.0
        IL_0026: ldftn instance void ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::'<InstanceMethod>b__0'()
        IL_002c: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
        IL_0031: stfld class [mscorlib]System.Action ConsoleApplication1.Program/Foo::_field
        IL_0036: nop
        IL_0037: ret
    }

示例3:

class Program
{
    static void Main(string[] args)
    {
    }

    static void Test(double arg)
    {

    }

    class Foo
    {
        private Action _field;

        public void InstanceMethod()
        {
            var capturedVariable = Math.Pow(42, 1);

            _field = () => Test(capturedVariable);  

        }

        private static void StaticMethod(double arg) { }
    }


}

在IL中:(此指针已返回)


IL_0006: ldloc.0
IL_0007: ldarg.0
IL_0008: stfld class ConsoleApplication1.Program/Foo ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::'<>4__this' //Back again.

在这三种情况下,方法b__0()的写法是相同的:

instance void '<InstanceMethod>b__0' () cil managed 
    {
        // Method begins at RVA 0x2066
        // Code size 13 (0xd)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldfld float64 ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::capturedVariable
                   IL_0006: call void ConsoleApplication1.Program/Foo::StaticMethod(float64) //Your example
                    IL_0006: call void ConsoleApplication1.Program/Foo2::StaticMethod(float64)//Example 2
        IL_0006: call void ConsoleApplication1.Program::Test(float64) //Example 3
        IL_000b: nop
        IL_000c: ret
    }

在所有三种情况下,都有一个对静态方法的引用,这使得它更加奇怪。因此,在进行了这个小分析之后,我会说这是一个错误/无意义的操作!


我猜这意味着在由嵌套类生成的 lambda 表达式中使用父类中的静态方法是一个不好的想法?我只是想知道如果将 Foo.InstanceMethod 设为静态,是否也会去除引用?如有了解,敬请告知。 - Ivaylo Slavov
1
@Ivaylo:如果Foo.InstanceMethod也是静态的,那么就不会有任何实例存在,因此也就没有任何方式可以通过闭包捕获this - Ani
1
如果实例方法是静态的,那么字段必须是静态的,我尝试过了 - 不会有“this指针”。@Ivaylo Slavov - Niklas
@Niklas,谢谢。总之,我认为使用静态方法创建lambda将确保不需要这个无用的指针。 - Ivaylo Slavov
@Ivaylo Slavov,我猜也是这样。 :) - Niklas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接