Roslyn gagal mengkompilasi kode

95

Setelah saya memindahkan proyek saya dari VS2013 ke VS2015, proyek tersebut tidak lagi dibangun. Terjadi galat kompilasi dalam pernyataan LINQ berikut ini:

static void Main(string[] args)
{
    decimal a, b;
    IEnumerable<dynamic> array = new string[] { "10", "20", "30" };
    var result = (from v in array
                  where decimal.TryParse(v, out a) && decimal.TryParse("15", out b) && a <= b // Error here
                  orderby decimal.Parse(v)
                  select v).ToArray();
}

Kompilator mengembalikan kesalahan:

Kesalahan CS0165 Penggunaan variabel lokal 'b' yang tidak ditetapkan

Apa penyebab masalah ini? Apakah mungkin untuk memperbaikinya melalui pengaturan kompilator?

ramil89
sumber
11
@BinaryWorrier: Mengapa? Ini hanya digunakan bsetelah menetapkannya melalui outparameter.
Jon Skeet
1
Dokumentasi VS 2015 mengatakan "Meskipun variabel yang dikirimkan sebagai argumen tidak harus diinisialisasi sebelum diteruskan, metode yang dipanggil diperlukan untuk menetapkan nilai sebelum metode kembali." jadi ini memang terlihat seperti bug ya, dijamin akan dijalankan oleh tryParse itu.
Rup
3
Terlepas dari kesalahannya, kode ini mencontohkan semua hal buruk tentang outargumen. Apakah itu TryParsemengembalikan nilai nullable (atau setara).
Konrad Rudolph
1
@KonradRudol where (a = decimal.TryParse(v)).HasValue && (b = decimal.TryParse(v)).HasValue && a <= bterlihat jauh lebih baik
Raw
2
Sekadar catatan, Anda bisa menyederhanakan ini menjadi decimal a, b; var q = decimal.TryParse((dynamic)"10", out a) && decimal.TryParse("15", out b) && a <= b;. Saya telah membuka bug Roslyn yang mengangkat ini.
Rawling

Jawaban:

112

Apa penyebab masalah ini?

Sepertinya bug kompiler bagi saya. Setidaknya, memang begitu. Meskipun ekspresi decimal.TryParse(v, out a)dan decimal.TryParse(v, out b)dievaluasi secara dinamis, saya berharap kompilator masih memahami bahwa pada saat mencapai a <= b, keduanya adan bsudah pasti ditetapkan. Bahkan dengan keanehan yang dapat Anda temukan dalam pengetikan dinamis, saya berharap untuk hanya mengevaluasi a <= bsetelah mengevaluasi kedua TryParsepanggilan tersebut.

Namun, ternyata melalui operator dan konversi yang rumit, sangat mungkin untuk memiliki ekspresi A && B && Cyang mengevaluasi Adan Ctetapi tidak B- jika Anda cukup licik. Lihat laporan bug Roslyn untuk contoh cerdik Neal Gafter.

Membuatnya bekerja dengan dynamiclebih sulit - semantik yang terlibat saat operan dinamis lebih sulit untuk dijelaskan, karena untuk melakukan resolusi kelebihan beban, Anda perlu mengevaluasi operan untuk mengetahui jenis apa yang terlibat, yang dapat menjadi kontra-intuitif. Namun, sekali lagi Neal memberikan contoh yang menunjukkan bahwa kesalahan kompilator diperlukan ... ini bukan bug, ini perbaikan bug . Pujian dalam jumlah besar untuk Neal karena telah membuktikannya.

Apakah mungkin untuk memperbaikinya melalui pengaturan kompiler?

Tidak, tetapi ada alternatif yang menghindari kesalahan tersebut.

Pertama, Anda dapat menghentikannya agar tidak dinamis - jika Anda tahu bahwa Anda hanya akan pernah menggunakan string, maka Anda dapat menggunakan IEnumerable<string> atau memberikan variabel rentang vjenis string(yaitu from string v in array). Itu akan menjadi pilihan yang saya sukai.

Jika Anda benar - benar perlu menjaganya tetap dinamis, berikan bnilai sebagai permulaan:

decimal a, b = 0m;

Ini tidak akan merugikan - kami tahu bahwa sebenarnya evaluasi dinamis Anda tidak akan melakukan sesuatu yang gila, jadi Anda tetap akan menetapkan nilai bsebelum Anda menggunakannya, membuat nilai awal tidak relevan.

Selain itu, tampaknya menambahkan tanda kurung juga berfungsi:

where decimal.TryParse(v, out a) && (decimal.TryParse("15", out b) && a <= b)

Itu mengubah titik di mana berbagai bagian resolusi berlebih dipicu, dan kebetulan membuat kompiler senang.

Ada satu masalah yang masih tersisa - aturan spesifikasi tentang penetapan pasti dengan &&operator perlu diklarifikasi untuk menyatakan bahwa mereka hanya berlaku ketika &&operator sedang digunakan dalam implementasi "reguler" dengan dua booloperan. Saya akan mencoba untuk memastikan ini diperbaiki untuk standar ECMA berikutnya.

Jon Skeet
sumber
Ya! Menerapkan IEnumerable<string>atau menambahkan tanda kurung berhasil untuk saya. Sekarang kompilator membangun tanpa kesalahan.
ramil89
1
menggunakan decimal a, b = 0m;mungkin menghapus kesalahan, tapi a <= bakan selalu digunakan 0m, karena nilai keluar belum dihitung.
Paw Baltzersen
12
@PawBaltzersen: Apa yang membuat Anda berpikir begitu? Itu selalu akan diberikan sebelum perbandingan - hanya saja kompiler tidak dapat membuktikannya, karena beberapa alasan (bug, pada dasarnya).
Jon Skeet
1
Memiliki metode parsing tanpa efek samping yaitu. decimal? TryParseDecimal(string txt)mungkin solusi juga
zahir
1
Saya ingin tahu apakah inisialisasi malas; ia berpikir "jika yang pertama benar maka saya tidak perlu mengevaluasi yang kedua yang artinya bmungkin tidak diberikan"; Saya tahu itu alasan yang tidak valid tetapi menjelaskan mengapa tanda kurung memperbaikinya ...
durron597
21

Ini tampaknya bug, atau setidaknya regresi, dalam kompiler Roslyn. Bug berikut telah diajukan untuk melacaknya:

https://github.com/dotnet/roslyn/issues/4509

Sementara itu, jawaban bagus Jon memiliki beberapa solusi.

JaredPar
sumber
Juga bug ini yang mencatat bahwa itu tidak ada hubungannya dengan LINQ ...
Rawling
16

Karena saya disekolahkan begitu keras dalam laporan bug, saya akan mencoba menjelaskannya sendiri.


Bayangkan Tadalah beberapa tipe yang ditentukan pengguna dengan pemeran implisit boolyang bergantian antara falsedan true, dimulai dengan false. Sejauh yang diketahui kompilator, dynamicargumen pertama ke argumen pertama &&mungkin mengevaluasi tipe itu, jadi itu harus pesimis.

Jika, kemudian, membiarkan kode dikompilasi, ini bisa terjadi:

  • Saat pengikat dinamis mengevaluasi yang pertama &&, ia melakukan hal berikut:
    • Evaluasi argumen pertama
    • Ini T- secara implisit melemparkannya ke bool.
    • Oh, ya false, jadi kita tidak perlu mengevaluasi argumen kedua.
    • Jadikan hasil &&evaluasi sebagai argumen pertama. (Tidak, tidak false, untuk beberapa alasan.)
  • Saat pengikat dinamis mengevaluasi yang kedua &&, ia melakukan hal berikut:
    • Evaluasi argumen pertama.
    • Ini T- secara implisit melemparkannya ke bool.
    • Oh, ya true, jadi evaluasi argumen kedua.
    • ... Oh sial, btidak ditugaskan.

Dalam istilah spesifikasi, singkatnya, ada aturan khusus "penugasan pasti" yang memungkinkan kita mengatakan tidak hanya apakah suatu variabel "ditetapkan secara pasti" atau "tidak ditetapkan secara pasti", tetapi juga jika ia "ditetapkan secara pasti setelah falsepernyataan" atau "pasti ditugaskan setelah truepernyataan ".

Ini ada sehingga ketika berhadapan dengan &&dan ||(dan !dan ??dan ?:) kompilator dapat memeriksa apakah variabel dapat ditugaskan di cabang tertentu dari ekspresi boolean kompleks.

Namun, ini hanya berfungsi jika tipe ekspresi tetap boolean . Jika bagian dari ekspresi adalah dynamic(atau tipe statis non-boolean), kami tidak dapat lagi mengatakan dengan andal bahwa ekspresi tersebut adalah trueatau false- saat berikutnya kami mentransmisikannya booluntuk memutuskan cabang mana yang akan diambil, mungkin telah berubah pikiran.


Pembaruan: ini sekarang telah diselesaikan dan didokumentasikan :

Aturan penetapan pasti yang diterapkan oleh kompiler sebelumnya untuk ekspresi dinamis memungkinkan beberapa kasus kode yang dapat mengakibatkan variabel yang dibaca tidak ditetapkan secara pasti. Lihat https://github.com/dotnet/roslyn/issues/4509 untuk satu laporan tentang ini.

...

Karena kemungkinan ini, kompilator tidak boleh mengizinkan program ini untuk dikompilasi jika val tidak memiliki nilai awal. Versi kompilator sebelumnya (sebelum VS2015) memungkinkan program ini untuk dikompilasi meskipun val tidak memiliki nilai awal. Roslyn sekarang mendiagnosis upaya untuk membaca variabel yang mungkin tidak diinisialisasi ini.

Rawling
sumber
1
Menggunakan VS2013 di komputer saya yang lain, saya benar-benar berhasil membaca memori yang belum ditetapkan menggunakan ini. Ini tidak terlalu menarik :(
Rawling
Anda dapat membaca variabel yang tidak diinisialisasi dengan simple delegate. Buat delegasi yang mendapatkan outmetode yang memiliki ref. Ini akan dengan senang hati melakukannya, dan itu akan membuat variabel ditugaskan, tanpa mengubah nilainya.
IllidanS4 ingin Monica kembali
Karena penasaran, saya menguji potongan itu dengan C # v4. Karena penasaran, bagaimana cara compiler memutuskan untuk menggunakan operator false/ truesebagai lawan dari operator cast implisit? Secara lokal, itu akan memanggil implicit operator boolargumen pertama, kemudian memanggil operan kedua, memanggil operator falseoperan pertama, diikuti oleh implicit operator boolpada operan pertama lagi . Ini tidak masuk akal bagi saya, operan pertama pada dasarnya harus mendidih menjadi boolean sekali, bukan?
Rob
@ Rob Apakah ini kasus dynamic, dirantai &&? Saya telah melihatnya pada dasarnya pergi (1) mengevaluasi argumen pertama (2) menggunakan pemeran implisit untuk melihat apakah saya dapat korsleting (3) Saya tidak bisa, jadi hindari argumen kedua (4) sekarang saya tahu kedua jenis, saya dapat melihat yang terbaik &&adalah &operator panggilan yang ditentukan pengguna (5) falsepada argumen pertama untuk melihat apakah saya dapat hubungan pendek (6) saya bisa (karena falsedan implicit booltidak setuju), jadi hasilnya adalah argumen pertama ... dan kemudian berikutnya &&, (7) gunakan pemeran implisit untuk melihat apakah saya dapat mengalami korsleting (lagi).
Rawling
@ IllidanS4 Kedengarannya menarik, tetapi saya belum menemukan cara melakukannya. Bisakah Anda memberi saya potongan?
Rawling
15

Ini bukan bug. Lihat https://github.com/dotnet/roslyn/issues/4509#issuecomment-130872713 untuk contoh bagaimana ekspresi dinamis dari formulir ini dapat membiarkan variabel seperti itu tidak ditetapkan.

Neal Gafter
sumber
1
Karena jawaban saya diterima dan diberi suara tinggi, saya telah mengeditnya untuk menunjukkan resolusi. Terima kasih atas semua pekerjaan Anda dalam hal ini - termasuk menjelaskan kesalahan saya kepada saya :)
Jon Skeet