Akses ke variabel foreach dalam peringatan penutupan

86

Saya mendapatkan peringatan berikut:

Akses ke variabel foreach di closure. Mungkin memiliki perilaku yang berbeda ketika dikompilasi dengan versi kompilator yang berbeda.

Seperti inilah tampilannya di editor saya:

pesan kesalahan yang disebutkan di atas dalam popup hover

Saya tahu cara memperbaiki peringatan ini, tetapi saya ingin tahu mengapa saya mendapatkan peringatan ini?

Apakah ini tentang versi "CLR"? Apakah itu terkait dengan "IL"?

Jeroen
sumber
1
TL; DR jawaban: tambahkan .ToList () atau .ToArray () di akhir ekspresi kueri Anda dan itu akan menghilangkan peringatan
JoelFan

Jawaban:

136

Ada dua bagian dari peringatan ini. Yang pertama adalah ...

Akses ke variabel foreach di closure

... yang sebenarnya tidak valid tetapi pada pandangan pertama kontra-intuitif. Juga sangat sulit untuk melakukan yang benar. (Sedemikian rupa sehingga artikel yang saya tautkan di bawah menggambarkan ini sebagai "berbahaya".)

Ambil pertanyaan Anda, perhatikan bahwa kode yang Anda kutipan pada dasarnya adalah bentuk yang diperluas dari apa yang dihasilkan oleh C # compiler (sebelum C # 5) untuk foreach1 :

Saya [tidak] mengerti mengapa [berikut ini] tidak valid:

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

Yah, itu valid secara sintaksis. Dan jika semua yang Anda lakukan dalam lingkaran Anda menggunakan nilai dari smaka semuanya baik. Tetapi penutupan sakan mengarah pada perilaku kontra-intuitif. Perhatikan kode berikut:

var countingActions = new List<Action>();

var numbers = from n in Enumerable.Range(1, 5)
              select n.ToString(CultureInfo.InvariantCulture);

using (var enumerator = numbers.GetEnumerator())
{
    string s;

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

        Console.WriteLine("Creating an action where s == {0}", s);
        Action action = () => Console.WriteLine("s == {0}", s);

        countingActions.Add(action);
    }
}

Jika Anda menjalankan kode ini, Anda akan mendapatkan keluaran konsol berikut:

Creating an action where s == 1
Creating an action where s == 2
Creating an action where s == 3
Creating an action where s == 4
Creating an action where s == 5

Inilah yang Anda harapkan.

Untuk melihat sesuatu yang mungkin tidak Anda harapkan, jalankan kode berikut segera setelah kode di atas:

foreach (var action in countingActions)
    action();

Anda akan mendapatkan keluaran konsol berikut:

s == 5
s == 5
s == 5
s == 5
s == 5

Mengapa? Karena kita membuat lima fungsi yang semuanya melakukan hal yang persis sama: mencetak nilai s(yang telah kita tutup). Pada kenyataannya, mereka memiliki fungsi yang sama ("Cetak s", "Cetak s", "Cetak s" ...).

Pada titik di mana kita akan menggunakannya, mereka melakukan apa yang kita minta: mencetak nilai s. Jika Anda melihat nilai terakhir yang diketahui dari s, Anda akan melihatnya 5. Jadi kami s == 5dicetak lima kali ke konsol.

Itulah tepatnya yang kita minta, tapi mungkin bukan yang kita inginkan.

Bagian kedua dari peringatan ...

Mungkin memiliki perilaku yang berbeda ketika dikompilasi dengan versi kompilator yang berbeda.

... adalah apa adanya. Dimulai dengan C # 5, kompilator menghasilkan kode berbeda yang "mencegah" ini terjadi melaluiforeach .

Dengan demikian kode berikut akan menghasilkan hasil yang berbeda di bawah versi kompiler yang berbeda:

foreach (var n in numbers)
{
    Action action = () => Console.WriteLine("n == {0}", n);
    countingActions.Add(action);
}

Akibatnya, itu juga akan menghasilkan peringatan R # :)

Potongan kode pertama saya, di atas, akan menunjukkan perilaku yang sama di semua versi kompilator, karena saya tidak menggunakan foreach(sebaliknya, saya telah mengembangkannya seperti yang dilakukan oleh kompiler pra-C # 5).

Apakah ini untuk versi CLR?

Saya tidak begitu yakin apa yang Anda tanyakan di sini.

Posting Eric Lippert mengatakan perubahan terjadi "di C # 5". Begitumungkin Anda harus menargetkan .NET 4.5 atau yang lebih baru dengan kompiler C # 5 atau yang lebih baru untuk mendapatkan perilaku baru, dan semua yang sebelumnya mendapatkan perilaku lama.

Tetapi untuk lebih jelasnya, ini adalah fungsi dari kompiler dan bukan versi .NET Framework.

Apakah ada relevansinya dengan IL?

Kode yang berbeda menghasilkan IL yang berbeda sehingga dalam hal ini terdapat konsekuensi untuk IL yang dihasilkan.

1 foreach adalah konstruksi yang jauh lebih umum daripada kode yang Anda posting di komentar Anda. Masalah biasanya muncul melalui penggunaan foreach, bukan melalui pencacahan manual. Itulah mengapa perubahan foreachpada C # 5 membantu mencegah masalah ini, tetapi tidak sepenuhnya.

ta.speot.is
sumber
7
Saya sebenarnya sudah mencoba foreach loop pada kompiler berbeda yang mendapatkan hasil berbeda menggunakan target yang sama (.Net 3.5). Saya menggunakan VS2010 (yang pada gilirannya menggunakan kompilator yang terkait dengan .net 4.0 saya percaya) dan VS2012 (kompiler .net 4.5 saya percaya). Pada prinsipnya, ini berarti bahwa jika Anda menggunakan VS2013 dan mengedit proyek yang menargetkan .Net 3.5 dan membangunnya di server build yang memiliki kerangka kerja yang sedikit lebih lama terpasang, Anda dapat melihat hasil yang berbeda dari program Anda di komputer vs. build yang diterapkan.
Ykok
Jawaban yang bagus, tetapi tidak yakin bagaimana "foreach" itu relevan. Bukankah ini terjadi dengan pencacahan manual, atau bahkan sederhana untuk (int i = 0; i <collection.Size; i ++) loop? Tampaknya menjadi masalah dengan closure yang keluar dari ruang lingkup, atau lebih tepatnya, masalah dengan orang yang memahami bagaimana closure berperilaku ketika mereka keluar dari ruang lingkup yang mereka definisikan di dalamnya.
Brad
Hal- foreachhal di sini berasal dari isi pertanyaan. Anda benar bahwa itu bisa terjadi dalam berbagai cara yang lebih umum.
ta.speot.adalah
1
Mengapa R # masih memperingatkan saya, bukankah itu membaca kerangka target, yang telah saya setel ke 4.5.
Johnny_D
1
"Jadi mungkin Anda harus menargetkan .NET 4.5 atau yang lebih baru" Pernyataan ini tidak benar. Versi .NET yang Anda targetkan tidak berpengaruh pada hal ini, perilakunya juga berubah di .NET 2.0, 3.5, dan 4 jika Anda menggunakan C # 5 (VS 2012 atau yang lebih baru) untuk mengkompilasi. Itulah mengapa Anda hanya mendapatkan peringatan ini pada .NET 4.0 atau yang lebih lama, jika Anda menargetkan 4.5 Anda tidak mendapatkan peringatan tersebut karena Anda tidak dapat mengkompilasi 4.5 pada kompiler C # 4 atau yang lebih lama.
Scott Chamberlain
12

Jawaban pertama bagus, jadi saya pikir saya hanya akan menambahkan satu hal.

Anda mendapatkan peringatan karena, dalam kode contoh Anda, reflectModel diberi IEnumerable, yang hanya akan dievaluasi pada saat enumerasi, dan enumerasi itu sendiri dapat terjadi di luar loop jika Anda menetapkan reflectModel ke sesuatu dengan cakupan yang lebih luas .

Jika Anda berubah

...Where(x => x.Name == property.Value)

untuk

...Where(x => x.Name == property.Value).ToList()

kemudian reflectModel akan diberi daftar tertentu dalam loop foreach, jadi Anda tidak akan menerima peringatan, karena enumerasi pasti akan terjadi di dalam loop, dan bukan di luar itu.

David
sumber
Saya membaca banyak penjelasan yang sangat panjang yang tidak menyelesaikan masalah ini untuk saya, lalu satu penjelasan singkat yang berhasil. Terima kasih!
Charles Clayton
Saya membaca jawaban yang diterima dan hanya berpikir "bagaimana bisa ditutup jika tidak mengikat variabel?" tapi sekarang saya mengerti ini tentang kapan evaluasi terjadi, terima kasih!
Jerome
Ya, ini adalah solusi universal yang jelas. Lambat, intensif memori, tapi menurut saya ini benar-benar 100% berfungsi untuk semua kasus.
Al Kepp
8

Variabel dengan cakupan blok harus menyelesaikan peringatan.

foreach (var entry in entries)
{
   var en = entry; 
   var result = DoSomeAction(o => o.Action(en));
}
Dmitry Gogol
sumber