Akses ke Penutupan yang Dimodifikasi (2)

101

Ini adalah pertanyaan tambahan dari Access to Modified Closure . Saya hanya ingin memverifikasi apakah berikut ini sebenarnya cukup aman untuk penggunaan produksi.

List<string> lists = new List<string>();
//Code to retrieve lists from DB    
foreach (string list in lists)
{
    Button btn = new Button();
    btn.Click += new EventHandler(delegate { MessageBox.Show(list); });
}

Saya hanya menjalankan di atas sekali per startup. Untuk saat ini tampaknya berfungsi dengan baik. Seperti yang telah disebutkan Jon tentang hasil yang berlawanan dengan intuisi dalam beberapa kasus. Jadi apa yang perlu saya waspadai di sini? Apakah akan baik-baik saja jika daftar dijalankan lebih dari sekali?

salah
sumber
18
Selamat, Anda sekarang menjadi bagian dari dokumentasi Resharper. confluence.jetbrains.net/display/ReSharper/…
Kongress
1
Yang ini rumit tetapi penjelasan di atas menjelaskan bagi saya: Ini mungkin tampak benar tetapi, pada kenyataannya, hanya nilai terakhir variabel str yang akan digunakan setiap kali tombol apa pun diklik. Alasannya adalah karena foreach membuka gulungan ke loop while, tetapi variabel iterasi ditentukan di luar loop ini. Ini berarti bahwa pada saat Anda menampilkan kotak pesan, nilai str mungkin sudah diiterasi ke nilai terakhir dalam kumpulan string.
DanielV

Jawaban:

159

Sebelum C # 5, Anda perlu mendeklarasikan ulang sebuah variabel di dalam foreach - jika tidak maka akan dibagikan, dan semua penangan Anda akan menggunakan string terakhir:

foreach (string list in lists)
{
    string tmp = list;
    Button btn = new Button();
    btn.Click += new EventHandler(delegate { MessageBox.Show(tmp); });
}

Secara signifikan, perhatikan bahwa dari C # 5 dan seterusnya, ini telah berubah, dan khususnya dalam kasusforeach , Anda tidak perlu melakukan ini lagi: kode dalam pertanyaan akan bekerja seperti yang diharapkan.

Untuk menunjukkan bahwa ini tidak berfungsi tanpa perubahan ini, pertimbangkan hal berikut:

string[] names = { "Fred", "Barney", "Betty", "Wilma" };
using (Form form = new Form())
{
    foreach (string name in names)
    {
        Button btn = new Button();
        btn.Text = name;
        btn.Click += delegate
        {
            MessageBox.Show(form, name);
        };
        btn.Dock = DockStyle.Top;
        form.Controls.Add(btn);
    }
    Application.Run(form);
}

Jalankan cara di atas sebelum C # 5 , dan meskipun setiap tombol menunjukkan nama yang berbeda, mengklik tombol tersebut menunjukkan "Wilma" empat kali.

Ini karena spesifikasi bahasa (ECMA 334 v4, 15.8.4) (sebelum C # 5) mendefinisikan:

foreach (V v in x) embedded-statement kemudian diperluas menjadi:

{
    E e = ((C)(x)).GetEnumerator();
    try {
        V v;
         while (e.MoveNext()) {
            v = (V)(T)e.Current;
             embedded-statement
        }
    }
    finally {
         // Dispose e
    }
}

Perhatikan bahwa variabel v(yaitu milik Anda list) dideklarasikan di luar perulangan. Jadi berdasarkan aturan variabel yang ditangkap, semua iterasi daftar akan berbagi pemegang variabel yang ditangkap.

Dari C # 5 dan seterusnya, ini diubah: variabel iterasi ( v) dicakup di dalam loop. Saya tidak memiliki referensi spesifikasi, tetapi pada dasarnya menjadi:

{
    E e = ((C)(x)).GetEnumerator();
    try {
        while (e.MoveNext()) {
            V v = (V)(T)e.Current;
            embedded-statement
        }
    }
    finally {
         // Dispose e
    }
}

Berhenti berlangganan kembali; jika Anda secara aktif ingin berhenti berlangganan penangan anonim, triknya adalah dengan menangkap penangan itu sendiri:

EventHandler foo = delegate {...code...};
obj.SomeEvent += foo;
...
obj.SomeEvent -= foo;

Demikian juga, jika Anda menginginkan penanganan kejadian sekali saja (seperti Muat dll):

EventHandler bar = null; // necessary for "definite assignment"
bar = delegate {
  // ... code
  obj.SomeEvent -= bar;
};
obj.SomeEvent += bar;

Ini sekarang berhenti berlangganan sendiri ;-p

Marc Gravell
sumber
Jika demikian, variabel sementara akan tetap berada di memori hingga aplikasi ditutup, untuk melayani delegasi, dan tidak disarankan untuk melakukannya untuk loop yang sangat besar jika variabel tersebut memakan banyak memori. Apakah saya benar?
rusak
1
Itu akan tetap dalam memori selama ada hal-hal (tombol) dengan acara tersebut. Ada cara untuk berhenti berlangganan delegasi hanya sekali, yang akan saya tambahkan ke postingan.
Marc Gravell
2
Tetapi untuk memenuhi syarat pada poin Anda: ya, variabel yang ditangkap memang dapat meningkatkan cakupan variabel. Anda harus berhati-hati untuk tidak menangkap hal-hal yang tidak Anda harapkan ...
Marc Gravell
1
Bisakah Anda memperbarui jawaban Anda sehubungan dengan perubahan dalam spesifikasi C # 5.0? Hanya untuk menjadikannya sebagai dokumentasi wiki yang bagus tentang foreach loop di C #. Sudah ada beberapa jawaban yang bagus tentang perubahan dalam kompilator C # 5.0 yang memperlakukan foreach loops bit.ly/WzBV3L , tetapi mereka bukan sumber yang mirip wiki.
Ilya Ivanov
1
@Kos ya, fortidak berubah di 5.0
Marc Gravell