这个对象生存期延长关闭是C#编译器错误吗?


136

我当时回答的问题有关闭的可能(合法)扩展对象的生命周期,当我遇到了一些非常奇怪的代码生成的C#编译器的部分(4.0,如果该事项)。

我能找到的最短的再现是:

  1. 创建一个lambda来捕获本地,同时调用包含类型的 静态方法。
  2. 将生成的委托引用分配给包含对象的实例字段。

结果:编译器在没有理由的情况下创建了一个引用创建lambda的对象的闭包对象-委托的“内部”目标是静态方法,而lambda-createing对象的实例成员不需要执行委托时(不被)触碰。实际上,编译器的行为就像程序员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
有趣。对我来说似乎是个虫子。请注意,如果您未分配给实例字段(例如,如果您返回值),则不会捕获this
乔恩·斯基特

15
我无法使用VS11 Developer预览对此进行复制。可以在VS2010SP1中复制。似乎是固定的:)
leppie 2011年

2
在VS2008SP1中也会发生这种情况。对于VS2010SP1,它适用于3.5和4.0。
leppie 2011年

5
嗯,错误是个大词。编译器只会生成效率低下的代码。当然不是泄漏,此垃圾收集没有问题。当他们致力于异步实现时,它可能已修复。
汉斯·帕桑

7
@Hans,如果委托可以在对象的生存期内生存下来,那么这将不会毫无问题地进行垃圾收集,并且没有什么可以阻止这种情况的发生。
SoftMemes

Answers:


24

那肯定看起来像个错误。感谢您引起我的注意。我会调查一下。可能已经找到并修复了它。


7

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

我在IL lang中以你为例:

.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中:(注意!现在此引用已消失!)

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
    }

在这3种情况下,都引用了一个静态方法,因此它变得更加奇怪。因此,在进行了一次小小的分析之后,我会说这是一个错误/没有好处。!


我想这意味着在嵌套类生成的lambda表达式中使用父类中的静态方法是一个坏主意吗?我只是想知道是否Foo.InstanceMethod设为静态,这还会删除引用吗?我很高兴知道。
Ivaylo Slavov

1
@Ivaylo:如果Foo.InstanceMethod还是静态的,则看不到实例,因此this闭包无法捕获任何形式的实例。
安仁

1
@Ivaylo Slavov如果实例方法是静态的,则该字段必须是静态的,我确实尝试过-并且不会有“ this指针”。
尼古拉斯

@尼古拉斯,谢谢。总之,我认为用于创建lambda的静态方法将确保缺少此不必要的指针。
Ivaylo Slavov

@Ivaylo Slavov,猜猜是.. :)
Niklas
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.