Catatan: ini tampaknya telah diperbaiki di Roslyn
Pertanyaan ini muncul ketika menulis jawaban saya untuk yang ini , yang berbicara tentang asosiatifitas operator penggabungan nol .
Sama seperti pengingat, ide dari operator null-coalescing adalah ekspresi dari form
x ?? y
pertama mengevaluasi x
, lalu:
- Jika nilai
x
nol,y
dievaluasi dan itu adalah hasil akhir dari ekspresi - Jika nilai
x
non-null,y
yang tidak dievaluasi, dan nilaix
adalah hasil akhir dari ekspresi, setelah konversi ke jenis kompilasi-waktuy
jika diperlukan
Sekarang biasanya tidak perlu konversi, atau itu hanya dari jenis nullable ke non-nullable - biasanya jenisnya sama, atau hanya dari (katakanlah) int?
ke int
. Namun, Anda dapat membuat operator konversi tersirat Anda sendiri, dan itu digunakan jika perlu.
Untuk kasus sederhana x ?? y
, saya belum melihat perilaku aneh. Namun, dengan (x ?? y) ?? z
saya melihat beberapa perilaku membingungkan.
Berikut ini adalah program pengujian singkat tapi lengkap - hasilnya ada di komentar:
using System;
public struct A
{
public static implicit operator B(A input)
{
Console.WriteLine("A to B");
return new B();
}
public static implicit operator C(A input)
{
Console.WriteLine("A to C");
return new C();
}
}
public struct B
{
public static implicit operator C(B input)
{
Console.WriteLine("B to C");
return new C();
}
}
public struct C {}
class Test
{
static void Main()
{
A? x = new A();
B? y = new B();
C? z = new C();
C zNotNull = new C();
Console.WriteLine("First case");
// This prints
// A to B
// A to B
// B to C
C? first = (x ?? y) ?? z;
Console.WriteLine("Second case");
// This prints
// A to B
// B to C
var tmp = x ?? y;
C? second = tmp ?? z;
Console.WriteLine("Third case");
// This prints
// A to B
// B to C
C? third = (x ?? y) ?? zNotNull;
}
}
Jadi kami memiliki tiga tipe nilai khusus A
,, B
dan C
, dengan konversi dari A ke B, A ke C, dan B ke C.
Saya dapat memahami kasus kedua dan ketiga ... tetapi mengapa ada konversi tambahan A ke B dalam kasus pertama? Khususnya, saya benar - benar berharap kasus pertama dan kedua menjadi hal yang sama - setelah itu, hanya mengekstraksi ekspresi menjadi variabel lokal.
Adakah yang tahu apa yang terjadi? Saya sangat ragu untuk menangis "bug" ketika datang ke kompiler C #, tapi saya bingung apa yang terjadi ...
EDIT: Oke, ini contoh buruk tentang apa yang terjadi, berkat jawaban konfigurator, yang memberi saya alasan lebih lanjut untuk menganggapnya sebagai bug. Sunting: Sampel bahkan tidak perlu dua operator penggabungan nol sekarang ...
using System;
public struct A
{
public static implicit operator int(A input)
{
Console.WriteLine("A to int");
return 10;
}
}
class Test
{
static A? Foo()
{
Console.WriteLine("Foo() called");
return new A();
}
static void Main()
{
int? y = 10;
int? result = Foo() ?? y;
}
}
Output dari ini adalah:
Foo() called
Foo() called
A to int
Fakta bahwa Foo()
dipanggil dua kali di sini sangat mengejutkan bagi saya - saya tidak dapat melihat alasan mengapa ekspresi dievaluasi dua kali.
sumber
C? first = ((B?)(((B?)x) ?? ((B?)y))) ?? ((C?)z);
. Anda akan mendapatkan:Internal Compiler Error: likely culprit is 'CODEGEN'
(("working value" ?? "user default") ?? "system default")
Jawaban:
Terima kasih kepada semua orang yang berkontribusi dalam menganalisis masalah ini. Ini jelas merupakan bug penyusun. Tampaknya hanya terjadi ketika ada konversi terangkat yang melibatkan dua jenis nullable di sisi kiri operator penggabungan.
Saya belum mengidentifikasi di mana tepatnya ada yang salah, tetapi pada beberapa titik selama fase kompilasi "nullable menurunkan" - setelah analisis awal tetapi sebelum pembuatan kode - kami mengurangi ekspresi
dari contoh di atas hingga yang setara dengan moral:
Jelas itu tidak benar; penurunan yang benar adalah
Tebakan terbaik saya berdasarkan analisis saya sejauh ini adalah bahwa pengoptimal nullable keluar dari jalur di sini. Kami memiliki pengoptimal yang dapat dibatalkan yang mencari situasi di mana kami tahu bahwa ekspresi tertentu dari jenis yang tidak dapat dibatalkan tidak mungkin nol. Pertimbangkan analisis naif berikut: pertama-tama kita mungkin mengatakan itu
sama dengan
dan kemudian kita bisa mengatakan itu
sama dengan
Tetapi pengoptimal dapat melangkah dan berkata "tunggu dulu, kami sudah memeriksa bahwa temp bukan nol; tidak perlu memeriksanya untuk null untuk yang kedua kalinya hanya karena kami memanggil operator konversi yang diangkat". Kami ingin mereka mengoptimalkannya menjadi adil
Dugaan saya adalah bahwa kita berada di suatu tempat yang menyembunyikan fakta bahwa bentuk yang dioptimalkan
(int?)Foo()
adalahnew int?(op_implicit(Foo().Value))
tetapi itu sebenarnya bukan bentuk yang dioptimalkan yang kita inginkan; kami ingin bentuk yang dioptimalkan dari Foo () - diganti-dengan-sementara-dan-kemudian dikonversi.Banyak bug di kompiler C # adalah hasil dari keputusan caching yang buruk. Sebuah kata bijak: setiap kali Anda menyimpan fakta untuk digunakan nanti, Anda berpotensi menciptakan inkonsistensi jika sesuatu yang relevan berubah . Dalam hal ini hal yang relevan yang telah berubah pasca analisis awal adalah bahwa panggilan ke Foo () harus selalu direalisasikan sebagai pengambilan sementara.
Kami melakukan banyak reorganisasi dari pass penulisan ulang nullable di C # 3.0. Bug mereproduksi dalam C # 3.0 dan 4.0 tetapi tidak dalam C # 2.0, yang berarti bahwa bug itu mungkin buruk saya. Maaf!
Saya akan memasukkan bug ke dalam basis data dan kami akan melihat apakah kami dapat memperbaikinya untuk versi bahasa yang akan datang. Sekali lagi terima kasih untuk analisis Anda; itu sangat membantu!
PEMBARUAN: Saya menulis ulang pengoptimal yang dapat dibatalkan dari awal untuk Roslyn; sekarang melakukan pekerjaan yang lebih baik dan menghindari kesalahan aneh semacam ini. Untuk beberapa pemikiran tentang cara kerja pengoptimal di Roslyn, lihat seri artikel saya yang dimulai di sini: https://ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one/
sumber
Ini pasti bug.
Kode ini akan menampilkan:
Itu membuat saya berpikir bahwa bagian pertama dari setiap
??
ekspresi penggabungan dievaluasi dua kali. Kode ini membuktikannya:output:
Ini tampaknya terjadi hanya ketika ekspresi membutuhkan konversi antara dua jenis nullable; Saya telah mencoba berbagai permutasi dengan salah satu sisi menjadi string, dan tidak ada satupun yang menyebabkan perilaku ini.
sumber
X() ?? Y()
berkembang secara internalX() != null ? X() : Y()
, maka mengapa itu akan dievaluasi dua kali.Jika Anda melihat pada kode yang dibuat untuk case yang dikelompokkan Kiri itu benar-benar melakukan sesuatu seperti ini (
csc /optimize-
):Menemukan lain, jika Anda menggunakannya
first
akan menghasilkan jalan pintas jika keduanyaa
danb
adalah null dan kembalic
. Namun jikaa
ataub
tidak nol itu mengevaluasi kembalia
sebagai bagian dari konversi implisit untukB
sebelum mengembalikan yanga
ataub
tidak nol.Dari Spesifikasi C # 4.0, §6.1.4:
Ini tampaknya menjelaskan kombinasi pembungkusan-pembungkus kedua.
Kompiler C # 2008 dan 2010 menghasilkan kode yang sangat mirip, namun ini terlihat seperti regresi dari kompiler C # 2005 (8.00.50727.4927) yang menghasilkan kode berikut untuk yang di atas:
Aku ingin tahu apakah ini bukan karena sihir tambahan yang diberikan pada sistem inferensi tipe?
sumber
(x ?? y) ?? z
menjadi lambda bersarang, yang memastikan evaluasi in-order tanpa evaluasi ganda. Ini jelas bukan pendekatan yang diambil oleh kompiler C # 4.0. Dari apa yang dapat saya katakan, bagian 6.1.4 didekati dengan cara yang sangat ketat dalam jalur kode khusus ini dan sementara waktu tidak dielakkan sehingga menghasilkan evaluasi ganda.Sebenarnya, saya akan menyebutnya bug sekarang, dengan contoh yang lebih jelas. Ini masih berlaku, tetapi evaluasi ganda jelas tidak baik.
Tampaknya seolah-olah
A ?? B
diimplementasikan sebagaiA.HasValue ? A : B
. Dalam hal ini, ada banyak casting juga (mengikuti casting reguler untuk?:
operator ternary ). Tetapi jika Anda mengabaikan semua itu, maka ini masuk akal berdasarkan cara penerapannya:A ?? B
meluas keA.HasValue ? A : B
A
adalah milik kitax ?? y
. Luaskan kex.HasValue : x ? y
(x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B
Di sini Anda dapat melihat bahwa
x.HasValue
dicentang dua kali, dan jikax ?? y
membutuhkan casting,x
akan dicasting dua kali.Saya meletakkannya hanya sebagai artefak tentang bagaimanaTake-Away: Jangan membuat operator casting implisit dengan efek samping.??
diimplementasikan, daripada bug kompiler.Tampaknya menjadi kompiler bug yang berputar di sekitar bagaimana
??
diterapkan. Take-away: jangan bersarang ekspresi penggabungan dengan efek samping.sumber
A() ? A() : B()
mungkin akan mengevaluasiA()
dua kali, tetapiA() ?? B()
tidak begitu banyak. Dan karena itu hanya terjadi pada casting ... Hmm .. Saya baru saja berbicara pada diri saya sendiri untuk berpikir itu pasti tidak berlaku dengan benar.Saya bukan ahli C # sama sekali seperti yang Anda lihat dari riwayat pertanyaan saya, tetapi, saya mencoba ini dan saya pikir ini adalah bug .... tetapi sebagai pemula, saya harus mengatakan bahwa saya tidak mengerti semuanya berjalan di sini jadi saya akan menghapus jawaban saya jika saya jauh.
Saya sampai pada
bug
kesimpulan ini dengan membuat versi berbeda dari program Anda yang berhubungan dengan skenario yang sama, tetapi tidak terlalu rumit.Saya menggunakan tiga properti integer nol dengan backing store. Saya mengatur masing-masing ke 4 dan kemudian menjalankan
int? something2 = (A ?? B) ?? C;
( Kode lengkap di sini )
Ini hanya membaca A dan tidak ada yang lain.
Pernyataan ini bagi saya sepertinya bagi saya seharusnya:
Jadi, karena A bukan nol, itu hanya melihat A dan selesai.
Dalam contoh Anda, meletakkan breakpoint pada Kasus Pertama menunjukkan bahwa x, y dan z semuanya tidak nol dan oleh karena itu, saya berharap mereka diperlakukan sama seperti contoh saya yang kurang kompleks .... tapi saya takut saya terlalu banyak seorang pemula C # dan telah melewatkan inti pertanyaan ini sepenuhnya!
sumber
int
). Dia mendorong case lebih jauh ke sudut yang tidak jelas dengan menyediakan beberapa konversi tipe implisit. Ini membutuhkan kompiler untuk mengubah jenis data saat memeriksanull
. Karena konversi tipe implisit inilah contohnya berbeda dari contoh Anda.