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

Posted

技术标签:

【中文标题】这个对象生命周期扩展闭包是 C# 编译器错误吗?【英文标题】:Is this object-lifetime-extending-closure a C# compiler bug? 【发布时间】:2012-01-15 04:28:31 【问题描述】:

我在回答 question 关于闭包(合法地)延长对象生命周期的可能性时遇到了一些非常好奇的 C# 编译器代码生成(如果是 4.0)事项)。

我能找到的最短的复制品如下:

    创建一个 lambda,在调用包含类型的 静态 方法时捕获局部变量。 将生成的委托引用分配给包含对象的 instance 字段。

结果:编译器创建了一个闭包对象,它引用了创建 lambda 的对象,当它没有理由时 - 委托的“内部”目标是一个 static 方法,并且执行委托时,不需要(也不会)触及 lambda-creating-object 的实例成员。实际上,编译器的行为就像程序员无缘无故捕获了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);
    

观察到闭包对象的&lt;&gt;4__this 字段填充了对象引用,但从未从中读取(没有理由)。

那么这里发生了什么?语言规范允许吗?这是一个编译器错误/怪异,还是有充分的理由(我显然错过了)让闭包引用该对象?这让我很焦虑,因为这看起来像是喜欢关闭的程序员(比如我)在不经意间将奇怪的内存泄漏(想象一下,如果委托被用作事件处理程序)引入程序的秘诀。

【问题讨论】:

有趣。对我来说似乎是一个错误。请注意,如果您不分配给实例字段(例如,如果您返回值),它不会捕获this 我无法使用 VS11 开发者预览版重现此内容。可以在 VS2010SP1 中重现。似乎它是固定的:) 这也发生在 VS2008SP1 中。对于 VS2010SP1,3.5 和 4.0 都会发生这种情况。 嗯,bug 是一个非常大的词,适用于此。编译器只是生成效率稍低的代码。当然不是泄漏,这种垃圾收集没有问题。当他们处理异步实现时,它可能已经修复了。 @Hans,如果委托能够在对象的生命周期内存活,那么这不会毫无问题地进行垃圾收集,并且没有什么可以阻止这种情况的发生。 【参考方案1】:

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

我用 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 中:(注意!!现在这个引用已经消失了!)

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:如果Foo.InstanceMethod 也是静态的,那么就看不到实例,因此任何类型的this 都无法被闭包捕获。 @Ivaylo Slavov 如果实例方法是静态的,那么该字段必须是静态的,我确实尝试过 - 并且不会有“this 指针”。 @Niklas,谢谢。总之,我认为创建 lambdas 的静态方法将保证没有这个不必要的指针。 @Ivaylo Slavov,猜猜看.. :)【参考方案2】:

这确实看起来像一个错误。感谢您引起我的注意。我会调查的。它可能已经被找到并修复了。

【讨论】:

以上是关于这个对象生命周期扩展闭包是 C# 编译器错误吗?的主要内容,如果未能解决你的问题,请参考以下文章

Rust 中的闭包生命周期通过简单的模式

《精通C#》第十三章 对象的生命周期

如何使用结构和隐式生命周期来推断实现的适当生命周期?

阻止使用静态生命周期创建对象

c++类中 各种成员的生命周期?

3. 闭包_对象组合继承模式_事件轮询机制