Saya bertemu masalah menarik tentang C #. Saya punya kode seperti di bawah ini.
List<Func<int>> actions = new List<Func<int>>();
int variable = 0;
while (variable < 5)
{
actions.Add(() => variable * 2);
++ variable;
}
foreach (var act in actions)
{
Console.WriteLine(act.Invoke());
}
Saya berharap untuk menampilkan 0, 2, 4, 6, 8. Namun, sebenarnya output lima 10s.
Tampaknya karena semua tindakan mengacu pada satu variabel yang ditangkap. Akibatnya, ketika mereka dipanggil, mereka semua memiliki output yang sama.
Apakah ada cara untuk mengatasi batasan ini agar setiap instance tindakan memiliki variabel yang ditangkap sendiri?
c#
closures
captured-variable
Morgan Cheng
sumber
sumber
Captured variables are always evaluated when the delegate is actually invoked, not when the variables were captured
.Jawaban:
Ya - ambil salinan variabel di dalam loop:
Anda dapat menganggapnya seolah-olah kompiler C # membuat variabel lokal "baru" setiap kali hits deklarasi variabel. Bahkan itu akan membuat objek penutupan baru yang sesuai, dan itu menjadi rumit (dalam hal implementasi) jika Anda merujuk ke variabel dalam beberapa lingkup, tetapi ia bekerja :)
Perhatikan bahwa kejadian yang lebih umum dari masalah ini adalah menggunakan
for
atauforeach
:Lihat bagian 7.14.4.2 dari spesifikasi C # 3.0 untuk detail lebih lanjut tentang ini, dan artikel saya tentang penutupan memiliki lebih banyak contoh juga.
Perhatikan bahwa pada kompiler C # 5 dan seterusnya (bahkan ketika menentukan versi C # sebelumnya), perilaku
foreach
berubah sehingga Anda tidak perlu lagi membuat salinan lokal. Lihat jawaban ini untuk lebih jelasnya.sumber
Saya percaya apa yang Anda alami adalah sesuatu yang dikenal sebagai Penutupan http://en.wikipedia.org/wiki/Closure_(computer_science) . Lamba Anda memiliki referensi ke variabel yang dicakup di luar fungsi itu sendiri. Lamba Anda tidak ditafsirkan sampai Anda memintanya dan setelah itu akan mendapatkan nilai variabel pada waktu eksekusi.
sumber
Di belakang layar, kompiler menghasilkan kelas yang mewakili penutupan untuk pemanggilan metode Anda. Ia menggunakan instance tunggal dari kelas penutupan untuk setiap iterasi dari loop. Kode terlihat seperti ini, yang membuatnya lebih mudah untuk melihat mengapa bug terjadi:
Ini sebenarnya bukan kode yang dikompilasi dari sampel Anda, tetapi saya telah memeriksa kode saya sendiri dan ini sangat mirip dengan apa yang sebenarnya dihasilkan oleh kompiler.
sumber
Cara mengatasinya adalah dengan menyimpan nilai yang Anda butuhkan dalam variabel proxy, dan membuat variabel itu ditangkap.
YAITU
sumber
Ini tidak ada hubungannya dengan loop.
Perilaku ini dipicu karena Anda menggunakan ekspresi lambda di
() => variable * 2
manavariable
cakupan luar tidak benar-benar didefinisikan dalam cakupan dalam lambda.Ekspresi Lambda (dalam C # 3 +, serta metode anonim dalam C # 2) masih membuat metode yang sebenarnya. Melewati variabel ke metode ini melibatkan beberapa dilema (lulus dengan nilai? Lulus dengan referensi? C # berjalan dengan referensi - tetapi ini membuka masalah lain di mana referensi dapat bertahan lebih lama dari variabel yang sebenarnya). Apa yang dilakukan C # untuk menyelesaikan semua dilema ini adalah membuat kelas pembantu baru ("closure") dengan bidang yang sesuai dengan variabel lokal yang digunakan dalam ekspresi lambda, dan metode yang sesuai dengan metode lambda yang sebenarnya. Setiap perubahan
variable
dalam kode Anda sebenarnya diterjemahkan untuk mengubahnyaClosureClass.variable
Jadi loop sementara Anda terus memperbarui
ClosureClass.variable
hingga mencapai 10, maka Anda untuk loop menjalankan tindakan, yang semuanya beroperasi pada saat yang samaClosureClass.variable
.Untuk mendapatkan hasil yang diharapkan, Anda perlu membuat pemisahan antara variabel loop, dan variabel yang sedang ditutup. Anda dapat melakukan ini dengan memperkenalkan variabel lain, yaitu:
Anda juga bisa memindahkan penutupan ke metode lain untuk membuat pemisahan ini:
Anda dapat menerapkan Mult sebagai ekspresi lambda (penutupan implisit)
atau dengan kelas pembantu sebenarnya:
Dalam setiap kasus, "Penutupan" BUKAN sebuah konsep yang terkait dengan loop , tetapi lebih kepada metode anonim / ekspresi lambda yang menggunakan variabel lingkup lokal - meskipun beberapa penggunaan loop yang tidak hati-hati menunjukkan perangkap penutup.
sumber
Ya, Anda perlu lingkup
variable
dalam loop dan meneruskannya ke lambda seperti itu:sumber
Situasi yang sama terjadi di multi-threading (C #, .NET 4.0].
Lihat kode berikut:
Tujuannya adalah untuk mencetak 1,2,3,4,5 secara berurutan.
Outputnya menarik! (Mungkin seperti 21334 ...)
Satu-satunya solusi adalah menggunakan variabel lokal.
sumber
Karena tidak ada seorang pun di sini yang langsung mengutip ECMA-334 :
Lebih lanjut dalam spesifikasi,
Oh ya, saya kira harus disebutkan bahwa dalam C ++ masalah ini tidak terjadi karena Anda dapat memilih apakah variabel ditangkap oleh nilai atau dengan referensi (lihat: Lambda capture ).
sumber
Ini disebut masalah penutupan, cukup gunakan variabel salin, dan selesai.
sumber