Apakah object-lifetime-extending-closure ini merupakan bug compiler C #?

136

Saya menjawab pertanyaan tentang kemungkinan penutupan (secara sah) memperpanjang masa hidup objek ketika saya bertemu dengan beberapa kode-gen yang sangat aneh pada bagian dari kompiler C # (4.0 jika itu penting).

Repro terpendek yang bisa saya temukan adalah sebagai berikut:

  1. Buat lambda yang menangkap lokal sambil memanggil metode statis dari tipe yang memuatnya.
  2. Tetapkan referensi-delegasi yang dihasilkan ke bidang instance dari objek yang memuat.

Hasil: Kompilator membuat objek-penutup yang mereferensikan objek yang membuat lambda, jika tidak memiliki alasan untuk - target 'dalam' dari delegasi adalah metode statis , dan anggota instance objek-pembuatan-lambda tidak perlu menjadi (dan tidak) disentuh saat delegasi dieksekusi. Secara efektif, kompilator bertindak seperti pemrogram menangkap thistanpa alasan.

class Foo
{
    private Action _field;

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

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

    private static void StaticMethod(double arg) { }
}

Kode yang dihasilkan dari rilis build (didekompilasi menjadi 'lebih sederhana' C #) terlihat seperti ini:

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

Amati bahwa <>4__thisbidang objek penutupan diisi dengan referensi objek tetapi tidak pernah dibaca (tidak ada alasan).

Jadi apa yang terjadi disini? Apakah spesifikasi bahasa memungkinkan untuk itu? Apakah ini bug / keanehan kompilator atau adakah alasan bagus (bahwa saya jelas hilang) untuk closure untuk mereferensikan objek? Ini membuat saya cemas karena ini terlihat seperti resep bagi programmer yang senang menutup (seperti saya) untuk tanpa disadari memperkenalkan kebocoran memori yang aneh (bayangkan jika delegasi digunakan sebagai penangan acara) ke dalam program.

Ani
sumber
19
Menarik. Sepertinya bug bagi saya. Perhatikan bahwa jika Anda tidak menetapkan ke bidang contoh (misalnya jika Anda mengembalikan nilai), itu tidak menangkap this.
Jon Skeet
15
Saya tidak dapat melakukan repro ini dengan pratinjau VS11 Developer. Dapat repro di VS2010SP1. Sepertinya sudah diperbaiki :)
leppie
2
Ini juga terjadi di VS2008SP1. Untuk VS2010SP1, ini terjadi untuk 3.5 dan 4.0.
leppie
5
Hmm, bug adalah kata yang sangat besar untuk diterapkan di sini. Kompiler hanya menghasilkan kode yang sedikit tidak efisien. Pastinya bukan kebocoran, sampah ini terkumpul tanpa masalah. Mungkin sudah diperbaiki saat mereka mengerjakan implementasi async.
Hans Passant
7
@Hans, ini tidak akan mengumpulkan sampah tanpa masalah jika delegasi akan selamat dari masa pakai objek, dan tidak ada yang mencegah hal ini terjadi.
SoftMemes

Jawaban:

24

Itu pasti terlihat seperti bug. Terima kasih telah memberitahukannya kepada saya. Saya akan memeriksanya. Mungkin saja itu sudah ditemukan dan diperbaiki.

Eric Lippert
sumber
7

Sepertinya bug atau tidak perlu:

Saya menjalankan Anda contoh dalam 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

Contoh 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) { }
    }


}

di cl: (Catatan !! sekarang referensi ini hilang!)

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
    }

Contoh 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) { }
    }


}

di IL: (Pointer ini kembali)

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

Dan dalam ketiga kasus metode-b__0 () - terlihat sama:

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
    }

Dan dalam ketiga kasus tersebut ada referensi ke metode statis, sehingga membuatnya lebih aneh. Jadi setelah analisis litle ini, saya akan mengatakan ini bug / tidak baik. !

Niklas
sumber
Saya kira ini berarti itu adalah ide yang buruk untuk menggunakan metode statis dari kelas induk di dalam ekspresi lambda yang dihasilkan oleh kelas bersarang? Saya hanya ingin tahu jika Foo.InstanceMethoddibuat statis, apakah ini juga akan menghapus referensi? Saya akan bersyukur mengetahuinya.
Ivaylo Slavov
1
@Ivaylo: Jika Foo.InstanceMethodjuga statis, tidak akan ada instance yang terlihat, dan oleh karena itu tidak ada cara untuk thisditangkap oleh closure.
Ani
1
@Ivaylo Slavov Jika metode instance statis, maka bidang harus statis, saya sudah mencoba - dan tidak akan ada 'pointer ini'.
Niklas
@Niklas, Terima kasih. Sebagai kesimpulan, saya kira metode statis untuk membuat lambda akan menjamin kurangnya penunjuk yang tidak perlu ini.
Ivaylo Slavov
@Ivaylo Slavov, Sepertinya begitu .. :)
Niklas