このオブジェクトの有効期限を延長するクロージャーはC#コンパイラのバグですか?


136

私がいくつかの極端に遭遇したとき、私はオブジェクトのライフタイムを(合法的に)延長する可能性についての質問に答えていましたC#コンパイラーの奇妙なコード生成(問題の場合は4.0。

私が見つけることができる最も短い再現は次のとおりです:

  1. staticを呼び出しているときにローカルをキャプチャするラムダを作成する 包含型のメソッド。
  2. 生成されたデリゲート参照を、それを含むオブジェクトのインスタンスフィールドに割り当てます。

結果:コンパイラーは、理由がない場合にラムダを作成したオブジェクトを参照するクロージャーオブジェクトを作成します。デリゲートの「内部」ターゲットは静的メソッドであり、ラムダ作成オブジェクトのインスタンスメンバーは不要です。デリゲートが実行されたときに触れられる(されない)。事実上、コンパイラーは、プログラマーが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
Jon Skeet、2011

15
VS11開発者プレビューではこれを再現できません。VS2010SP1で再現できます。それは修正されているようです:)
leppie

2
これはVS2008SP1でも発生します。VS2010SP1では、3.5と4.0の両方で発生します。
leppie

5
うーん、バグはこれに当てはまる非常に大きな言葉です。コンパイラはわずかに非効率的なコードを生成するだけです。確かにリークではなく、このガベージは問題なく収集されます。彼らが非同期実装に取り​​組んだときにおそらく修正されました。
ハンスパッサント

7
@ハンス、デリゲートがオブジェクトの存続期間を超えて存続する場合、これは問題なくガベージコレクションになりません。これが発生するのを妨げるものはありません。
SoftMemes 2011

回答:


24

それは確かにバグのように見えます。注目していただきありがとうございます。調べてみます。すでに発見され修正されている可能性があります。


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:(注!!現在、この参照はなくなりました!)

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.

そして、3つのケースすべてでmethod-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つのケースすべてに静的メソッドへの参照があるため、より奇妙になります。したがって、このlitle分析の後で、私はそれをバグといいます/良くありません !


これは、ネストされたクラスによって生成されたラムダ式内の親クラスの静的メソッドを使用するのは悪い考えだと思いますか?Foo.InstanceMethod静的にしたのかと思いますが、これで参照も削除されますか?知って感謝します。
Ivaylo Slavov、2011

1
@Ivaylo:Foo.InstanceMethod静的でもある場合、インスタンスが見えないためthis、クロージャーがインスタンスをキャプチャする方法はありません。
アニ

1
@Ivaylo Slavovインスタンスメソッドが静的である場合、フィールドは静的である必要があります。私は試してみました-「このポインタ」はありません。
Niklas

@Niklas、ありがとう。結論として、ラムダを作成するための静的メソッドは、この不要なポインターの欠如を保証すると思います。
Ivaylo Slavov、2011

@Ivaylo Slavov、そうだと思う.. :)
Niklas
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.