Mengapa ReSharper memberi tahu saya "penutupan yang ditangkap secara implisit"?

296

Saya memiliki kode berikut:

public double CalculateDailyProjectPullForceMax(DateTime date, string start = null, string end = null)
{
    Log("Calculating Daily Pull Force Max...");

    var pullForceList = start == null
                             ? _pullForce.Where((t, i) => _date[i] == date).ToList() // implicitly captured closure: end, start
                             : _pullForce.Where(
                                 (t, i) => _date[i] == date && DateTime.Compare(_time[i], DateTime.Parse(start)) > 0 && 
                                           DateTime.Compare(_time[i], DateTime.Parse(end)) < 0).ToList();

    _pullForceDailyMax = Math.Round(pullForceList.Max(), 2, MidpointRounding.AwayFromZero);

    return _pullForceDailyMax;
}

Sekarang, saya telah menambahkan komentar di jalur yang ReSharper sarankan perubahan. Apa artinya itu, atau mengapa harus diubah?implicitly captured closure: end, start

PiousVenom
sumber
6
MyCodeSucks tolong perbaiki jawaban yang diterima: jawaban kevingessner salah (seperti dijelaskan dalam komentar) dan ditandai sebagai diterima akan menyesatkan pengguna jika mereka tidak melihat jawaban Konsol.
Albireo
1
Anda juga dapat melihat ini jika Anda mendefinisikan daftar di luar try / catch dan melakukan semua penambahan Anda dalam try / catch dan kemudian mengatur hasilnya ke objek lain. Memindahkan definisi / penambahan dalam try / catch akan memungkinkan GC. Semoga ini masuk akal.
Micah Montoya

Jawaban:

391

Peringatan itu memberi tahu Anda bahwa variabel enddan starttetap hidup karena setiap lambda di dalam metode ini tetap hidup.

Lihatlah contoh singkatnya

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);

    int i = 0;
    Random g = new Random();
    this.button1.Click += (sender, args) => this.label1.Text = i++.ToString();
    this.button2.Click += (sender, args) => this.label1.Text = (g.Next() + i).ToString();
}

Saya mendapat peringatan "Penutupan yang ditangkap secara implisit: g" di lambda pertama. Ini memberi tahu saya bahwa gtidak bisa dikumpulkan sampah selama lambda pertama digunakan.

Kompiler menghasilkan kelas untuk kedua ekspresi lambda dan menempatkan semua variabel di kelas itu yang digunakan dalam ekspresi lambda.

Jadi dalam contoh saya gdan iditahan di kelas yang sama untuk eksekusi delegasi saya. Jika gbenda berat dengan banyak sumber daya tertinggal, pengumpul sampah tidak dapat mengklaimnya kembali, karena referensi di kelas ini masih hidup selama ada ekspresi lambda yang digunakan. Jadi ini potensi kebocoran memori, dan itulah alasan peringatan R #.

@plintor Seperti dalam C # metode anonim selalu disimpan dalam satu kelas per metode ada dua cara untuk menghindari ini:

  1. Gunakan metode instance daripada yang anonim.

  2. Pisahkan penciptaan ekspresi lambda menjadi dua metode.

Menghibur
sumber
30
Apa cara yang mungkin untuk menghindari penangkapan ini?
splintor
2
Terima kasih atas jawaban yang luar biasa ini - Saya telah belajar bahwa ada alasan untuk menggunakan metode non-anonim meskipun hanya digunakan di satu tempat.
ScottRhee
1
@splintor Instantiate objek di dalam delegasi, atau meneruskannya sebagai parameter. Dalam kasus di atas, sejauh yang saya tahu, perilaku yang diinginkan sebenarnya adalah untuk memegang referensi ke Randominstance.
Casey
2
@emodendroket Benar, pada titik ini kita berbicara gaya kode dan keterbacaan. Bidang lebih mudah untuk dipikirkan. Jika tekanan ingatan atau masa hidup objek penting, saya akan memilih bidang, jika tidak saya akan meninggalkannya dalam penutupan yang lebih ringkas.
yzorg
1
Kasing saya (sangat) disederhanakan menjadi metode pabrik yang menciptakan Foo dan Bar. Itu kemudian berlangganan menangkap lambas ke acara yang diekspos oleh dua objek dan, kejutan, Foo menjaga menangkap dari lamba acara Bar hidup dan sebaliknya. Saya berasal dari C ++ di mana pendekatan ini akan bekerja dengan baik, dan lebih dari sedikit heran menemukan aturan yang berbeda di sini. Semakin banyak Anda tahu, saya kira.
dlf
35

Setuju dengan Peter Mortensen.

Compiler C # hanya menghasilkan satu jenis yang merangkum semua variabel untuk semua ekspresi lambda dalam suatu metode.

Misalnya, diberi kode sumber:

public class ValueStore
{
    public Object GetValue()
    {
        return 1;
    }

    public void SetValue(Object obj)
    {
    }
}

public class ImplicitCaptureClosure
{
    public void Captured()
    {
        var x = new object();

        ValueStore store = new ValueStore();
        Action action = () => store.SetValue(x);
        Func<Object> f = () => store.GetValue();    //Implicitly capture closure: x
    }
}

Kompiler menghasilkan jenis seperti:

[CompilerGenerated]
private sealed class c__DisplayClass2
{
  public object x;
  public ValueStore store;

  public c__DisplayClass2()
  {
    base.ctor();
  }

  //Represents the first lambda expression: () => store.SetValue(x)
  public void Capturedb__0()
  {
    this.store.SetValue(this.x);
  }

  //Represents the second lambda expression: () => store.GetValue()
  public object Capturedb__1()
  {
    return this.store.GetValue();
  }
}

Dan Capturemetode ini dikompilasi sebagai:

public void Captured()
{
  ImplicitCaptureClosure.c__DisplayClass2 cDisplayClass2 = new ImplicitCaptureClosure.c__DisplayClass2();
  cDisplayClass2.x = new object();
  cDisplayClass2.store = new ValueStore();
  Action action = new Action((object) cDisplayClass2, __methodptr(Capturedb__0));
  Func<object> func = new Func<object>((object) cDisplayClass2, __methodptr(Capturedb__1));
}

Meskipun lambda kedua tidak digunakan x, itu tidak bisa menjadi sampah yang dikumpulkan karena xdikompilasi sebagai properti dari kelas yang dihasilkan yang digunakan dalam lambda.

Smartkid
sumber
31

Peringatan ini valid dan ditampilkan dalam metode yang memiliki lebih dari satu lambda , dan mereka menangkap nilai yang berbeda .

Ketika metode yang berisi lambdas dipanggil, objek yang dihasilkan kompiler dipakai dengan:

  • metode contoh yang mewakili lambda
  • bidang yang mewakili semua nilai yang ditangkap oleh salah satu lambda tersebut

Sebagai contoh:

class DecompileMe
{
    DecompileMe(Action<Action> callable1, Action<Action> callable2)
    {
        var p1 = 1;
        var p2 = "hello";

        callable1(() => p1++);    // WARNING: Implicitly captured closure: p2

        callable2(() => { p2.ToString(); p1++; });
    }
}

Periksa kode yang dihasilkan untuk kelas ini (dirapikan sedikit):

class DecompileMe
{
    DecompileMe(Action<Action> callable1, Action<Action> callable2)
    {
        var helper = new LambdaHelper();

        helper.p1 = 1;
        helper.p2 = "hello";

        callable1(helper.Lambda1);
        callable2(helper.Lambda2);
    }

    [CompilerGenerated]
    private sealed class LambdaHelper
    {
        public int p1;
        public string p2;

        public void Lambda1() { ++p1; }

        public void Lambda2() { p2.ToString(); ++p1; }
    }
}

Perhatikan contoh LambdaHelpertoko yang dibuat baik p1danp2 .

Bayangkan itu:

  • callable1 menyimpan referensi berumur panjang untuk argumennya, helper.Lambda1
  • callable2 tidak menyimpan referensi ke argumennya, helper.Lambda2

Dalam situasi ini, referensi helper.Lambda1juga secara tidak langsung merujuk string ke dalam p2, dan ini berarti bahwa pengumpul sampah tidak akan dapat membatalkan alokasi itu. Paling buruk itu adalah kebocoran memori / sumber daya. Selain itu, objek dapat tetap hidup lebih lama dari yang dibutuhkan, yang dapat berdampak pada GC jika dipromosikan dari gen0 ke gen1.

Drew Noakes
sumber
jika kita mengambil referensi p1dari callable2seperti ini: callable2(() => { p2.ToString(); });- apakah ini masih tidak menyebabkan masalah yang sama (pengumpul sampah tidak akan dapat membatalkan alokasi itu) karena LambdaHelpermasih mengandung p1dan p2?
Antony
1
Ya, masalah yang sama akan ada. Kompiler membuat satu objek tangkap (yaitu di LambdaHelperatas) untuk semua lambdas dalam metode induk. Jadi, bahkan jika callable2tidak digunakan p1, itu akan berbagi objek tangkapan yang sama callable1, dan objek tangkapan itu akan merujuk keduanya p1dan p2. Perhatikan bahwa ini hanya penting untuk tipe referensi, dan p1dalam contoh ini adalah tipe nilai.
Drew Noakes
3

Untuk kueri Linq ke Sql, Anda mungkin mendapatkan peringatan ini. Ruang lingkup lambda mungkin hidup lebih lama dari metode karena fakta bahwa permintaan sering diaktualisasikan setelah metode berada di luar cakupan. Bergantung pada situasi Anda, Anda mungkin ingin mengaktualisasikan hasil (yaitu melalui .ToList ()) dalam metode untuk memungkinkan GC pada vars instance metode yang ditangkap dalam L2S lambda.

Jason Dufair
sumber
2

Anda selalu dapat menemukan alasan R # saran hanya dengan mengklik petunjuk seperti yang ditunjukkan di bawah ini:

masukkan deskripsi gambar di sini

Petunjuk ini akan mengarahkan Anda ke sini .


Inspeksi ini menarik perhatian Anda pada fakta bahwa nilai penutupan lebih banyak ditangkap daripada yang terlihat jelas, yang berdampak pada masa pakai nilai-nilai ini.

Pertimbangkan kode berikut:

using System; 
public class Class1 {
    private Action _someAction;

    public void Method() {
        var obj1 = new object();
        var obj2 = new object();

        _someAction += () => {
            Console.WriteLine(obj1);
            Console.WriteLine(obj2);
        };

        // "Implicitly captured closure: obj2"
        _someAction += () => {
            Console.WriteLine(obj1);
        };
    }
}

Pada penutupan pertama, kita melihat bahwa kedua obj1 dan obj2 ditangkap secara eksplisit; kita bisa melihat ini hanya dengan melihat kodenya. Untuk penutupan kedua, kita dapat melihat bahwa obj1 sedang ditangkap secara eksplisit, tetapi ReSharper memperingatkan kita bahwa obj2 sedang ditangkap secara implisit.

Ini karena detail implementasi dalam kompiler C #. Selama kompilasi, penutupan ditulis ulang ke dalam kelas dengan bidang yang menyimpan nilai yang ditangkap, dan metode yang mewakili penutupan itu sendiri. Kompiler C # hanya akan membuat satu kelas privat per metode, dan jika lebih dari satu penutupan didefinisikan dalam suatu metode, maka kelas ini akan berisi beberapa metode, satu untuk setiap penutupan, dan itu juga akan mencakup semua nilai yang diambil dari semua penutupan.

Jika kita melihat kode yang dihasilkan oleh kompiler, tampilannya sedikit seperti ini (beberapa nama telah dibersihkan untuk memudahkan membaca):

public class Class1 {
    [CompilerGenerated]
    private sealed class <>c__DisplayClass1_0
    {
        public object obj1;
        public object obj2;

        internal void <Method>b__0()
        {
            Console.WriteLine(obj1);
            Console.WriteLine(obj2);
        }

        internal void <Method>b__1()
        {
            Console.WriteLine(obj1);
        }
    }

    private Action _someAction;

    public void Method()
    {
        // Create the display class - just one class for both closures
        var dc = new Class1.<>c__DisplayClass1_0();

        // Capture the closure values as fields on the display class
        dc.obj1 = new object();
        dc.obj2 = new object();

        // Add the display class methods as closure values
        _someAction += new Action(dc.<Method>b__0);
        _someAction += new Action(dc.<Method>b__1);
    }
}

Ketika metode berjalan, itu menciptakan kelas tampilan, yang menangkap semua nilai, untuk semua penutupan. Jadi, bahkan jika nilai tidak digunakan di salah satu penutupan, itu masih akan ditangkap. Ini adalah tangkapan "implisit" yang disoroti ReSharper.

Implikasi dari inspeksi ini adalah bahwa nilai penutupan yang ditangkap secara implisit tidak akan menjadi sampah yang dikumpulkan sampai penutupan itu sendiri adalah sampah yang dikumpulkan. Masa pakai nilai ini sekarang terkait dengan masa penutupan yang tidak menggunakan nilai secara eksplisit. Jika penutupan berumur panjang, ini mungkin memiliki efek negatif pada kode Anda, terutama jika nilai yang ditangkap sangat besar.

Perhatikan bahwa sementara ini adalah detail implementasi dari kompiler, ini konsisten di seluruh versi dan implementasi seperti Microsoft (sebelum dan sesudah Roslyn) atau kompiler Mono. Implementasi harus bekerja seperti yang dijelaskan untuk menangani dengan benar beberapa penutupan yang menangkap tipe nilai. Misalnya, jika beberapa penutupan menangkap int, maka mereka harus menangkap contoh yang sama, yang hanya dapat terjadi dengan satu kelas bersarang pribadi bersama. Efek sampingnya adalah masa pakai semua nilai yang ditangkap sekarang adalah masa pakai maksimum dari penutupan apa pun yang menangkap nilai apa pun.

anatol
sumber