Pertanyaan Apakah ada alasan untuk menggunakan kembali variabel C dalam sebuah foreach?


Ketika menggunakan ekspresi lambda atau metode anonim di C #, kita harus waspada terhadap akses ke penutupan yang dimodifikasi jebakan. Sebagai contoh:

foreach (var s in strings)
{
   query = query.Where(i => i.Prop == s); // access to modified closure
   ...
}

Karena penutupan yang dimodifikasi, kode di atas akan menyebabkan semua Where klausa pada kueri yang harus didasarkan pada nilai akhir s.

Seperti yang dijelaskan sini, ini terjadi karena s variabel yang dideklarasikan di foreach loop di atas diterjemahkan seperti ini di compiler:

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}

bukannya seperti ini:

while (enumerator.MoveNext())
{
   string s;
   s = enumerator.Current;
   ...
}

Seperti yang ditunjukkan sini, tidak ada keuntungan kinerja untuk mendeklarasikan variabel di luar loop, dan dalam keadaan normal satu-satunya alasan yang dapat saya pikirkan untuk melakukan ini adalah jika Anda berencana untuk menggunakan variabel di luar lingkup loop:

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}
var finalString = s;

Namun variabel didefinisikan dalam suatu foreach lingkaran tidak dapat digunakan di luar lingkaran:

foreach(string s in strings)
{
}
var finalString = s; // won't work: you're outside the scope.

Jadi compiler mendeklarasikan variabel dengan cara yang membuatnya sangat rentan terhadap kesalahan yang sering sulit ditemukan dan di-debug, sementara tidak menghasilkan manfaat yang dapat dilihat.

Apakah ada sesuatu yang bisa Anda lakukan? foreach loop dengan cara ini yang tidak bisa Anda lakukan jika mereka dikompilasi dengan variabel cakupan internal, atau apakah ini hanya pilihan acak yang dibuat sebelum metode anonim dan ekspresi lambda tersedia atau umum, dan yang belum direvisi sejak saat itu?


1482
2018-01-17 17:21


asal


Jawaban:


Compiler mendeklarasikan variabel dengan cara yang membuatnya sangat rentan terhadap kesalahan yang sering sulit ditemukan dan didebug, sementara tidak menghasilkan manfaat yang dapat dilihat.

Kritik Anda sepenuhnya dibenarkan.

Saya mendiskusikan masalah ini secara rinci di sini:

Menutup variabel loop dianggap berbahaya

Apakah ada sesuatu yang dapat Anda lakukan dengan loop foreach dengan cara ini yang tidak dapat Anda lakukan jika dikompilasi dengan variabel cakupan internal? atau apakah ini hanya pilihan acak yang dibuat sebelum metode anonim dan ekspresi lambda tersedia atau umum, dan yang belum direvisi sejak saat itu?

Yang terakhir. Spesifikasi C # 1.0 sebenarnya tidak mengatakan apakah variabel loop berada di dalam atau di luar badan loop, karena tidak ada perbedaan yang dapat diamati. Ketika penutupan semantik diperkenalkan di C # 2.0, pilihan dibuat untuk menempatkan variabel loop di luar loop, konsisten dengan "untuk" loop.

Saya pikir adil untuk mengatakan bahwa semua menyesali keputusan itu. Ini adalah salah satu "getchas" terburuk dalam C #, dan kita akan mengambil perubahan untuk memperbaikinya. Dalam C # 5 variabel loop foreach akan logis dalam tubuh loop, dan karena itu penutupan akan mendapatkan salinan baru setiap kali.

Itu for loop tidak akan diubah, dan perubahan tidak akan "kembali porting" ke versi C # sebelumnya. Karena itu Anda harus terus berhati-hati saat menggunakan idiom ini.


1279
2018-01-17 17:56



Apa yang Anda minta sepenuhnya tertutupi oleh Eric Lippert di posting blognya Menutup variabel loop dianggap berbahaya dan sekuelnya.

Bagi saya, argumen yang paling meyakinkan adalah bahwa memiliki variabel baru dalam setiap iterasi akan tidak konsisten for(;;)lingkaran gaya. Apakah Anda berharap memiliki yang baru int i dalam setiap iterasi for (int i = 0; i < 10; i++)?

Masalah yang paling umum dengan perilaku ini adalah membuat penutupan atas variabel iterasi dan memiliki solusi yang mudah:

foreach (var s in strings)
{
    var s_for_closure = s;
    query = query.Where(i => i.Prop == s_for_closure); // access to modified closure

Pos blog saya tentang masalah ini: Penutupan variabel foreach di C #.


174
2018-01-17 17:39



Setelah digigit oleh ini, saya memiliki kebiasaan untuk memasukkan variabel yang didefinisikan secara lokal di ruang lingkup terdalam yang saya gunakan untuk mentransfer ke penutupan apa pun. Dalam contoh Anda:

foreach (var s in strings)
{
    query = query.Where(i => i.Prop == s); // access to modified closure

Saya lakukan:

foreach (var s in strings)
{
    string search = s;
    query = query.Where(i => i.Prop == search); // New definition ensures unique per iteration.

Setelah Anda memiliki kebiasaan itu, Anda dapat menghindarinya di sangat Kasus yang jarang Anda benar-benar dimaksudkan untuk mengikat ke luar cakupan. Sejujurnya, saya tidak berpikir saya pernah melakukannya.


95
2018-01-17 17:47



Di C # 5.0, masalah ini diperbaiki dan Anda dapat menutup variabel loop dan mendapatkan hasil yang Anda harapkan.

Spesifikasi bahasa mengatakan:

8.8.4 Pernyataan foreach

(...)

Sebuah pernyataan foreach dari formulir

foreach (V v in x) embedded-statement

kemudian diperluas ke:

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

(...)

Penempatan v di dalam loop sementara penting untuk bagaimana itu   ditangkap oleh fungsi anonim apa pun yang terjadi di   pernyataan tertanam. Sebagai contoh:

int[] values = { 7, 9, 13 };
Action f = null;
foreach (var value in values)
{
    if (f == null) f = () => Console.WriteLine("First value: " + value);
}
f();

Jika v dideklarasikan di luar lingkaran sementara, itu akan dibagikan   di antara semua iterasi, dan nilainya setelah for loop akan menjadi   nilai akhir, 13, itulah yang disebut doa f akan mencetak.   Sebaliknya, karena setiap iterasi memiliki variabel tersendiri v, yang satu   ditangkap oleh f di iterasi pertama akan terus memegang nilai    7, yang akan dicetak. (Catatan: versi C # yang lebih lama   dideklarasikan v di luar loop sementara.)


52
2017-09-03 13:58



Menurut saya itu pertanyaan yang aneh. Sangat bagus untuk mengetahui cara kerja kompilator tetapi hanya "bagus untuk diketahui".

Jika Anda menulis kode yang bergantung pada algoritma compiler, itu adalah praktik yang buruk. Dan lebih baik menulis ulang kode untuk mengecualikan ketergantungan ini.

Ini pertanyaan bagus untuk wawancara kerja. Tetapi dalam kehidupan nyata, saya tidak menghadapi masalah yang saya pecahkan dalam wawancara kerja.

90% dari penggunaan foreach untuk memproses setiap elemen koleksi (bukan untuk memilih atau menghitung beberapa nilai). kadang-kadang Anda perlu menghitung beberapa nilai di dalam loop tetapi itu bukan praktik yang baik untuk membuat loop BESAR.

Lebih baik menggunakan ekspresi LINQ untuk menghitung nilai. Karena ketika Anda menghitung banyak hal di dalam loop, setelah 2-3 bulan ketika Anda (atau orang lain) akan membaca kode orang ini tidak akan mengerti apa ini dan bagaimana cara kerjanya.


0
2018-06-15 07:31