Mengapa saya dapat menetapkan 0,0 ke nilai pencacahan, tetapi tidak 1,0


90

Hanya ingin tahu: mengapa saya dapat menetapkan 0,0 ke variabel yang merupakan jenis enumerasi, tetapi tidak 1,0? Lihat kode berikut:

public enum Foo
{
    Bar,
    Baz
}

class Program
{
    static void Main()
    {
        Foo value1 = 0.0;
        Foo value2 = 1.0;   // This line does not compile
        Foo value3 = 4.2;   // This line does not compile
    }
}

Saya pikir konversi antara tipe numerik dan nilai enumerasi hanya diperbolehkan melalui cast? Artinya saya bisa menulis Foo value2 = (Foo) 1.0;sehingga baris 2 Maindapat dikompilasi. Mengapa ada pengecualian untuk nilai 0.0di C #?


17
Bagi saya itu aneh, Anda dapat menetapkan double literal 0.0 ke custom enum. Bukan berarti Anda tidak dapat menetapkan 1.0literal ke enum khusus.
Ilya Ivanov

2
Saya menduga kompilator memperlakukannya sebagai 0gantinya. Saya memiliki pertanyaan serupa sekali dan Rawling memposting jawaban yang bagus di sini .
Ramah

2
IdeOne tidak mengkompilasinya.
Johnny Mopp

Jawaban:


98

Ini adalah bug yang dapat Anda gunakan 0.0. Kompilator secara implisit memperlakukan semua ekspresi konstan dengan nilai nol hanya sebagai 0.

Sekarang, sudah benar bagi kompilator untuk mengizinkan konversi implisit dari intekspresi konstan 0 ke enum sesuai dengan bagian 6.1.3 spesifikasi C # 5:

Konversi enumerasi implisit memungkinkan desimal-integer-literal 0 untuk dikonversi ke jenis enum dan ke jenis nullable apa pun yang jenis dasarnya adalah jenis enum. Dalam kasus terakhir, konversi dievaluasi dengan mengubah ke tipe enum yang mendasarinya dan membungkus hasilnya (§4.1.10).

Saya telah berbicara dengan tim C # tentang ini sebelumnya: mereka ingin menghapus konversi yang tidak disengaja dari 0,0 (dan memang 0,0m dan 0,0f) menjadi nilai enum, tapi sayangnya saya mengumpulkannya terlalu banyak merusak kode - meskipun itu seharusnya tidak pernah diizinkan sejak awal.

Mono mcscompiler melarang semua ini konversi floating point, meskipun tidak memungkinkan:

const int Zero = 0;
...

SomeEnum x = Zero;

meskipun faktanya itu Zeroadalah ekspresi konstan tetapi bukan desimal-integer-literal.

Saya tidak akan terkejut melihat spesifikasi C # berubah di masa depan untuk memungkinkan ekspresi konstan integer dengan nilai 0 (yaitu untuk meniru mcs), tetapi saya tidak akan mengharapkan konversi floating point secara resmi benar. (Saya pernah salah sebelumnya tentang memprediksi masa depan C #, tentu saja ...)


3
Sesuai spesifikasi, ini hanya dimaksudkan untuk menjadi literal 0. Jadi harus menolak 1-1- intekspresi konstan dengan nilai 0. Tetapi seperti yang Anda amati, kompilator tidak sejalan dengan spesifikasi di sini.
Damien_The_Unbeliever

4
it broke too much code- Sangat sulit membayangkan alasan apa pun untuk menulis kode seperti itu.
Ilya Ivanov

1
@ObsidianPhoenix: Saya tidak yakin apa yang Anda maksud. Ini persis sama dengan: SomeEnum x = (SomeEnum) 0;. Itu adalah kasus apakah ada nilai nol bernama atau tidak.
Jon Skeet

2
@ObsidianPhoenix: Tidak, karena nilainya Test.Fooadalah 1, bukan 0 ... sekali lagi, itu persis sama seperti jika Anda menulis Test v1 = (Test) 0;- dan perilaku tersebut berlaku untuk nilai apa pun yang bukan nilai bernama dalam enum.
Jon Skeet

2
@ JonSkeet apakah ini akan diperbaiki di Roslyn?
Maksimal

98

Jawaban Jon benar. Saya akan menambahkan poin-poin berikut.

  • Saya menyebabkan serangga konyol dan memalukan ini. Banyak permintaan maaf.

  • Bug ini disebabkan oleh saya yang salah memahami semantik dari predikat "ekspresi adalah nol" dalam kompilator; Saya percaya itu memeriksa hanya untuk persamaan nol integer, padahal sebenarnya itu memeriksa lebih banyak di sepanjang baris "apakah ini nilai default dari jenis ini?" Faktanya, dalam versi bug sebelumnya, sebenarnya dimungkinkan untuk menetapkan nilai default dari tipe apa pun ke enum! Sekarang hanya nilai default angka. (Pelajaran: Beri nama predikat pembantu Anda dengan hati-hati.)

  • Perilaku yang saya coba terapkan yang saya buat kacau sebenarnya adalah solusi untuk bug yang sedikit berbeda. Anda dapat membaca seluruh kisah mengerikan di sini: https://docs.microsoft.com/en-us/archive/blogs/ericlippert/the-root-of-all-evil-part-one dan https://docs.microsoft .com / en-us / archive / blog / ericlippert / the-root-of-all-evil-part-two (Pelajaran: Sangat mudah untuk memperkenalkan bug baru yang lebih buruk saat memperbaiki yang lama.)

  • Tim C # memutuskan untuk mengabadikan perilaku buggy ini daripada memperbaikinya karena risiko melanggar kode yang ada tanpa keuntungan yang memaksa terlalu tinggi. (Pelajaran: lakukan dengan benar untuk pertama kalinya!)

  • Kode yang saya tulis di Roslyn untuk menjaga perilaku ini dapat ditemukan dalam metode IsConstantNumericZerodi https://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/ConversionsBase.cs - lihat untuk lebih jelasnya tentang apa sebenarnya perilaku Roslyn. Saya menulis hampir semua kode di direktori Konversi; Saya mendorong Anda untuk membaca semuanya karena ada banyak fakta menarik tentang bagaimana C # menyimpang dari spesifikasi di komentar. Saya menghias masing-masing dengan PELANGGARAN SPEC agar mudah ditemukan.

Satu lagi tempat menarik: C # juga memungkinkan nilai enum apa pun untuk digunakan dalam penginisialisasi enum terlepas dari ke nolnya:

enum E { A = 1 }
enum F { B = E.A }  // ???

Spesifikasi agak tidak jelas, apakah ini harus legal atau tidak, tetapi sekali lagi, karena ini telah ada di kompiler untuk waktu yang lama, kompiler baru cenderung mempertahankan perilaku tersebut.


10
Ini sangat keren, saya akhirnya bisa melihat kode yang Anda tulis. Mengagumkan bahwa kode sumber Roslyn adalah open source. Sekarang saya benar-benar memahami bahwa ada alasan yang valid (teknis / legal) untuk tidak memberikan riwayat perubahan tetapi akan sangat luar biasa melihat riwayat perubahan untuk melihat bagaimana kode berevolusi.
SolutionYogi

The C# team decided to enshrine this buggy behaviour rather than fixing it because the risk of breaking existing code for no compelling benefit was too high.Saya tidak berpikir akan ada banyak orang yang bergantung pada perilaku ini, dan salah satu keanehan inilah yang mungkin lebih baik untuk diperbaiki. Namun, itu tidak terlalu merugikan (kecuali untuk proyek yang menerapkan spesifikasi).
Aidiakapi

5
@Aidiakapi: Memang, jumlah orang yang terkena dampak harus sedikit; itu bukan nol. Tim C # menanggapi perubahan yang melanggar dengan sangat serius. Mudah bagi Anda untuk mengatakan bahwa lebih baik melakukan perbaikan; Anda tidak perlu berurusan dengan pelanggan yang marah yang menelepon wakil presiden Anda untuk mengeluh bahwa perubahan sepele Anda yang tidak menambah manfaat apa pun menunda integrasi sistem mereka sehari.
Eric Lippert

3
Lebih buruk. Semua perubahan yang merusak tersebut akan (idealnya) terdaftar di panduan migrasi Framework Microsoft. Semakin panjang daftar ini, semakin ragu pengguna untuk memigrasi aplikasi mereka. Jadi, bahkan perubahan kecil yang merusak menyebabkan: 1. Sejumlah kecil aplikasi rusak. 2. Sejumlah kecil pengguna menolak untuk meningkatkan (meskipun masalah tidak memengaruhi mereka). 3. Sejumlah kecil pengguna membuang sumber daya untuk mengevaluasi jika perubahan yang merusak mempengaruhi mereka. 4. Pengguna dari # 1, # 2, dan # 3 untuk mengeluh kepada orang lain.
Brian

@EricLippert Jika "Spesifikasi agak kabur", bukankah masuk akal untuk memperbarui spesifikasi? (Pertanyaan asli!)
Yakobus

10

Pencacahan di C # adalah dengan nilai integral definisi. Untuk konsistensi, C # seharusnya tidak menerima salah satu dari tugas ini, tetapi 0.0secara diam-diam diperlakukan sebagai integral 0. Ini mungkin peninggalan dari C, di mana literal 0diperlakukan secara khusus dan pada dasarnya dapat mengambil jenis tertentu - bilangan bulat, angka floating point, pointer nol ... sebut saja.


3
pertanyaannya adalah mengapa ? Jika Anda pergi ke IL- itu mendorong nilai integer ke tumpukanIL_0001: ldc.i4.0
Ilya Ivanov

@IlyaIvanov Lihat pembaruan. Tapi sejujurnya, jawabannya adalah “bukan alasan yang bagus”.
Konrad Rudolph

2
Saya pikir ini adalah salah satu kasus di mana jika Anda melihat spesifikasi C # , itu tidak legal, tetapi jika Anda melihat kompiler C # yang telah diproduksi MS, ia melakukan ini.
Damien_The_Unbeliever

3

enum benar-benar dimaksudkan (dalam semua bahasa yang mendukungnya) menjadi cara untuk bekerja dengan string yang bermakna dan unik (label) daripada nilai numerik. Jadi, dalam contoh Anda, Anda seharusnya hanya menggunakan Bar dan Baz saat berhadapan dengan tipe data enumerasi Foo . Anda tidak boleh menggunakan (bandingkan dengan, atau tetapkan) bilangan bulat, meskipun banyak kompiler akan membiarkan Anda menggunakannya (enum biasanya adalah bilangan bulat secara internal), dan dalam kasus ini, 0,0 secara sembarangan diperlakukan sebagai 0 oleh kompilator.

Secara konseptual, sebaiknya menambahkan bilangan bulat n ke nilai yang disebutkan, untuk mendapatkan nilai n lebih jauh ke bawah, atau menggunakan val2 - val1 untuk melihat seberapa jauh jaraknya, tetapi kecuali spesifikasi bahasa secara eksplisit mengizinkan ini, saya akan menghindarinya. (Pikirkan nilai yang disebutkan sebagai seperti penunjuk C, dengan cara Anda dapat menggunakannya.) Tidak ada alasan enum tidak dapat diimplementasikan dengan angka floating point, dan kenaikan tetap di antara mereka, tetapi saya belum pernah mendengarnya ini dilakukan dalam bahasa apa pun.


Saya tahu bahwa saya tidak boleh menggunakan enum seperti itu di C # - tetapi saya menemukan penggoda otak ini dan ingin tahu mengapa 0.0 berfungsi, tetapi 1.0 tidak. Saya tahu bahwa itu pasti sesuatu dengan kompiler C # karena Anda dapat melihat bahwa Kode IL untuk Foo v1 = 0.0;sama dengan untuk Foo v2 = Foo.Bar.
feO2x
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.