Mengapa tanda kurung konstruktor penginisialisasi objek C # 3.0 opsional?

114

Tampaknya sintaks penginisialisasi objek C # 3.0 memungkinkan seseorang untuk mengecualikan pasangan buka / tutup tanda kurung di konstruktor ketika ada konstruktor tanpa parameter. Contoh:

var x = new XTypeName { PropA = value, PropB = value };

Sebagai lawan:

var x = new XTypeName() { PropA = value, PropB = value };

Saya ingin tahu mengapa pasangan kurung buka / tutup konstruktor opsional di sini setelahnya XTypeName?

James Dunne
sumber
9
Selain itu, kami menemukan ini dalam tinjauan kode minggu lalu: var list = new List <Foo> {}; Jika sesuatu dapat disalahgunakan ...
blu
@blu Itulah salah satu alasan saya ingin menanyakan pertanyaan ini. Saya melihat adanya ketidakkonsistenan dalam kode kami. Inkonsistensi secara umum mengganggu saya jadi saya pikir saya akan melihat apakah ada alasan yang baik di balik opsionalitas dalam sintaks. :)
James Dunne

Jawaban:

143

Pertanyaan ini menjadi subjek blog saya pada tanggal 20 September 2010 . Jawaban Josh dan Chad ("mereka tidak menambah nilai jadi mengapa membutuhkannya?" Dan "untuk menghilangkan redundansi") pada dasarnya benar. Untuk menyempurnakannya sedikit lagi:

Fitur yang memungkinkan Anda untuk memilih daftar argumen sebagai bagian dari "fitur yang lebih besar" dari penginisialisasi objek memenuhi bilah kami untuk fitur "manis". Beberapa poin yang kami pertimbangkan:

  • biaya desain dan spesifikasi rendah
  • kami akan secara ekstensif mengubah kode parser yang menangani pembuatan objek; biaya pengembangan tambahan untuk membuat daftar parameter opsional tidak besar dibandingkan dengan biaya fitur yang lebih besar
  • beban pengujian relatif kecil dibandingkan dengan biaya fitur yang lebih besar
  • beban dokumentasi relatif kecil dibandingkan ...
  • beban pemeliharaan diantisipasi menjadi kecil; Saya tidak ingat ada bug yang dilaporkan dalam fitur ini selama bertahun-tahun sejak dikirim.
  • fitur tersebut tidak menimbulkan risiko langsung yang jelas terhadap fitur masa depan di area ini. (Hal terakhir yang ingin kami lakukan adalah membuat fitur yang murah dan mudah sekarang yang membuatnya lebih sulit untuk menerapkan fitur yang lebih menarik di masa mendatang.)
  • fitur tersebut tidak menambahkan ambiguitas baru pada analisis leksikal, gramatikal, atau semantik bahasa tersebut. Ini tidak menimbulkan masalah untuk jenis analisis "program parsial" yang dilakukan oleh mesin "IntelliSense" IDE saat Anda mengetik. Dan seterusnya.
  • fitur mencapai "sweet spot" umum untuk fitur inisialisasi objek yang lebih besar; biasanya jika Anda menggunakan penginisialisasi objek justru karena konstruktor objek tidak mengizinkan Anda untuk menyetel properti yang Anda inginkan. Sangat umum untuk objek seperti itu hanya menjadi "tas properti" yang tidak memiliki parameter di ctor sejak awal.

Lalu mengapa Anda tidak juga membuat tanda kurung kosong opsional dalam panggilan konstruktor default dari ekspresi pembuatan objek yang tidak memiliki penginisialisasi objek?

Perhatikan kembali daftar kriteria di atas. Salah satunya adalah bahwa perubahan tersebut tidak menimbulkan ambiguitas baru dalam analisis leksikal, gramatikal atau semantik suatu program. Perubahan yang Anda usulkan memang memperkenalkan ambiguitas analisis semantik:

class P
{
    class B
    {
        public class M { }
    }
    class C : B
    {
        new public void M(){}
    }
    static void Main()
    {
        new C().M(); // 1
        new C.M();   // 2
    }
}

Baris 1 membuat C baru, memanggil konstruktor default, lalu memanggil metode instance M pada objek baru. Baris 2 membuat instance baru BM dan memanggil konstruktor defaultnya. Jika tanda kurung pada baris 1 bersifat opsional maka baris 2 akan menjadi ambigu. Kami kemudian harus membuat aturan untuk menyelesaikan ambiguitas; kami tidak dapat membuatnya menjadi kesalahan karena itu akan menjadi perubahan yang merusak yang mengubah program C # legal yang ada menjadi program rusak.

Oleh karena itu, aturannya harus sangat rumit: pada dasarnya tanda kurung hanya opsional jika tidak menimbulkan ambiguitas. Kami harus menganalisis semua kemungkinan kasus yang menyebabkan ambiguitas dan kemudian menulis kode di compiler untuk mendeteksinya.

Dalam terang itu, kembali dan lihat semua biaya yang saya sebutkan. Berapa banyak dari mereka sekarang menjadi besar? Aturan yang rumit memiliki biaya desain, spesifikasi, pengembangan, pengujian, dan dokumentasi yang besar. Aturan yang rumit lebih cenderung menyebabkan masalah dengan interaksi tak terduga dengan fitur di masa mendatang.

Semuanya untuk apa? Manfaat pelanggan kecil yang tidak menambahkan kekuatan representasi baru ke bahasa, tetapi menambahkan kasus sudut gila hanya menunggu untuk meneriakkan "gotcha" pada beberapa jiwa malang yang tidak curiga yang menabraknya. Fitur seperti itu segera dipotong dan dimasukkan ke dalam daftar "jangan pernah lakukan ini".

Bagaimana Anda menentukan ambiguitas itu?

Yang itu segera menjadi jelas; Saya cukup akrab dengan aturan di C # untuk menentukan kapan nama putus-putus diharapkan.

Saat mempertimbangkan fitur baru, bagaimana Anda menentukan apakah fitur itu menyebabkan ambiguitas? Dengan tangan, dengan bukti resmi, dengan analisis mesin, apa?

Ketiganya. Sebagian besar kita hanya melihat spesifikasi dan mie di atasnya, seperti yang saya lakukan di atas. Misalnya, kita ingin menambahkan operator awalan baru ke C # yang disebut "frob":

x = frob 123 + 456;

(UPDATE: frobtentu saja await; analisis di sini pada dasarnya adalah analisis yang dilakukan tim desain saat menambahkan await.)

"frob" di sini seperti "baru" atau "++" - ia muncul sebelum semacam ekspresi. Kami akan mengerjakan prioritas dan asosiatif yang diinginkan dan seterusnya, dan kemudian mulai mengajukan pertanyaan seperti "bagaimana jika program sudah memiliki jenis, bidang, properti, peristiwa, metode, konstanta, atau lokal yang disebut frob?" Itu akan segera mengarah pada kasus seperti:

frob x = 10;

apakah itu berarti "lakukan operasi frob pada hasil x = 10, atau buat variabel berjenis frob yang disebut x dan tetapkan 10 untuk itu?" (Atau, jika frobbing menghasilkan variabel, itu bisa menjadi penugasan 10 ke frob x. Lagi pula, *x = 10;parsing dan legal jika xada int*.)

G(frob + x)

Apakah itu berarti "frob hasil dari operator plus unary pada x" atau "tambahkan ekspresi frob ke x"?

Dan seterusnya. Untuk mengatasi ambiguitas ini, kami mungkin memperkenalkan heuristik. Saat Anda mengucapkan "var x = 10;" itu ambigu; ini bisa berarti "menyimpulkan tipe dari x" atau bisa juga berarti "x adalah tipe var". Jadi kita memiliki heuristik: pertama kita mencoba mencari tipe bernama var, dan hanya jika tidak ada kita menyimpulkan tipe x.

Atau, kami mungkin mengubah sintaks agar tidak ambigu. Ketika mereka merancang C # 2.0, mereka mengalami masalah ini:

yield(x);

Apakah itu berarti "hasil x dalam sebuah iterator" atau "panggil metode hasil dengan argumen x?" Dengan mengubahnya menjadi

yield return(x);

sekarang tidak ambigu.

Dalam kasus parens opsional dalam penginisialisasi objek, sangat mudah untuk mempertimbangkan apakah ada ambiguitas yang diperkenalkan atau tidak karena jumlah situasi di mana diperbolehkan untuk memperkenalkan sesuatu yang dimulai dengan {sangat kecil . Pada dasarnya hanya berbagai konteks pernyataan, pernyataan lambdas, penginisialisasi array dan hanya itu. Sangat mudah untuk bernalar melalui semua kasus dan menunjukkan bahwa tidak ada ambiguitas. Memastikan IDE tetap efisien agak lebih sulit tetapi dapat dilakukan tanpa terlalu banyak masalah.

Mengotak-atik spesifikasi seperti ini biasanya sudah cukup. Jika itu adalah fitur yang sangat rumit maka kami mengeluarkan alat yang lebih berat. Misalnya, saat mendesain LINQ, salah satu orang kompiler dan salah satu orang IDE yang sama-sama memiliki latar belakang dalam teori parser membuat sendiri generator parser yang dapat menganalisis tata bahasa untuk mencari ambiguitas, dan kemudian memasukkan tata bahasa C # yang diusulkan untuk pemahaman kueri ke dalamnya. ; melakukan hal itu menemukan banyak kasus di mana kueri menjadi ambigu.

Atau, ketika kami melakukan inferensi tipe lanjutan pada lambda di C # 3.0 kami menulis proposal kami dan kemudian mengirimkannya ke Microsoft Research di Cambridge di mana tim bahasa di sana cukup baik untuk membuat bukti formal bahwa proposal inferensi tipe itu secara teoritis terdengar.

Apakah ada ambiguitas dalam C # hari ini?

Tentu.

G(F<A, B>(0))

Dalam C # 1 jelas apa artinya. Itu sama dengan:

G( (F<A), (B>0) )

Artinya, ia memanggil G dengan dua argumen yang disebut bools. Dalam C # 2, itu bisa berarti apa yang dimaksud dalam C # 1, tetapi bisa juga berarti "berikan 0 ke metode umum F yang mengambil parameter tipe A dan B, dan kemudian meneruskan hasil dari F ke G". Kami menambahkan heuristik rumit ke parser yang menentukan kasus mana yang mungkin Anda maksudkan.

Demikian pula, pemerannya ambigu bahkan di C # 1.0:

G((T)-x)

Apakah itu "cast -x to T" atau "kurangi x dari T"? Sekali lagi, kami memiliki heuristik yang membuat tebakan bagus.

Eric Lippert
sumber
3
Oh maaf, saya lupa ... Pendekatan sinyal kelelawar, meskipun tampaknya berhasil, lebih disukai daripada (IMO) alat kontak langsung di mana seseorang tidak akan mendapatkan paparan publik yang diinginkan untuk pendidikan publik dalam bentuk pos SO yang dapat diindeks, dapat ditelusuri, dan siap dirujuk. Haruskah kita menghubungi secara langsung untuk membuat koreografi tarian post / answer yang dipentaskan? :)
James Dunne
5
Saya menyarankan agar Anda menghindari pementasan posting. Itu tidak akan adil bagi orang lain yang mungkin memiliki wawasan tambahan tentang pertanyaan tersebut. Pendekatan yang lebih baik adalah mengeposkan pertanyaan, lalu mengirimkan tautan melalui email untuk meminta partisipasi.
chilltemp
1
@ James: Saya telah memperbarui jawaban saya untuk menjawab pertanyaan tindak lanjut Anda.
Eric Lippert
8
@Eric, apakah mungkin Anda dapat membuat blog tentang daftar "jangan pernah lakukan ini" ini? Saya penasaran melihat contoh lain yang tidak akan pernah menjadi bagian dari bahasa C # :)
Ilya Ryzhenkov
2
@ Eric: Aku benar-benar benar-benar menghargai kesabaran Anda dengan saya :) Terima kasih! Sangat informatif.
James Dunne
12

Karena begitulah bahasa itu ditentukan. Mereka tidak memberi nilai tambah, jadi mengapa menyertakannya?

Ini juga sangat mirip dengan array yang diketik implikasinya

var a = new[] { 1, 10, 100, 1000 };            // int[]
var b = new[] { 1, 1.5, 2, 2.5 };            // double[]
var c = new[] { "hello", null, "world" };      // string[]
var d = new[] { 1, "one", 2, "two" };         // Error

Referensi: http://msdn.microsoft.com/en-us/library/ms364047%28VS.80%29.aspx

CaffGeek
sumber
1
Mereka tidak menambahkan nilai dalam arti harus jelas apa maksudnya di sana, tetapi rusak dengan konsistensi karena sekarang kita memiliki dua sintaks konstruksi objek yang berbeda, satu dengan tanda kurung diperlukan (dan ekspresi argumen dipisahkan koma di dalamnya) dan satu tanpa .
James Dunne
1
@ James Dunne, sebenarnya sintaksnya sangat mirip dengan sintaks array yang diketik secara implisit, lihat edit saya. Tidak ada tipe, tidak ada konstruktor, dan maksudnya jelas, jadi tidak perlu mendeklarasikannya
CaffGeek
7

Ini dilakukan untuk menyederhanakan konstruksi objek. Desainer bahasa belum (sepengetahuan saya) secara khusus mengatakan mengapa mereka merasa ini berguna, meskipun secara eksplisit disebutkan di halaman Spesifikasi C # Versi 3.0 :

Ekspresi pembuatan objek bisa menghilangkan daftar argumen konstruktor dan menyertakan tanda kurung, asalkan itu menyertakan objek atau penginisialisasi koleksi. Menghilangkan daftar argumen konstruktor dan menyertakan tanda kurung sama dengan menentukan daftar argumen kosong.

Saya kira mereka merasa tanda kurung, dalam hal ini, tidak diperlukan untuk menunjukkan maksud pengembang, karena penginisialisasi objek menunjukkan maksud untuk membangun dan menyetel properti objek sebagai gantinya.

Reed Copsey
sumber
4

Dalam contoh pertama Anda, kompilator menyimpulkan bahwa Anda memanggil konstruktor default (Spesifikasi Bahasa C # 3.0 menyatakan bahwa jika tidak ada tanda kurung yang disediakan, konstruktor default dipanggil).

Yang kedua, Anda secara eksplisit memanggil konstruktor default.

Anda juga dapat menggunakan sintaks itu untuk menyetel properti sambil secara eksplisit meneruskan nilai ke konstruktor. Jika Anda memiliki definisi kelas berikut:

public class SomeTest
{
    public string Value { get; private set; }
    public string AnotherValue { get; set; }
    public string YetAnotherValue { get; set;}

    public SomeTest() { }

    public SomeTest(string value)
    {
        Value = value;
    }
}

Ketiga pernyataan itu valid:

var obj = new SomeTest { AnotherValue = "Hello", YetAnotherValue = "World" };
var obj = new SomeTest() { AnotherValue = "Hello", YetAnotherValue = "World"};
var obj = new SomeTest("Hello") { AnotherValue = "World", YetAnotherValue = "!"};
Justin Niessner
sumber
Baik. Dalam kasus pertama dan kedua dalam contoh Anda, ini secara fungsional identik, bukan?
James Dunne
1
@ James Dunne - Benar. Itu adalah bagian yang ditentukan oleh spesifikasi bahasa. Tanda kurung kosong berlebihan, tetapi Anda tetap dapat memasukkannya.
Justin Niessner
1

Saya bukan Eric Lippert, jadi saya tidak bisa mengatakan dengan pasti, tetapi saya akan berasumsi itu karena tanda kurung kosong tidak diperlukan oleh kompilator untuk menyimpulkan konstruksi inisialisasi. Oleh karena itu menjadi informasi yang mubazir, dan tidak diperlukan.

Josh
sumber
Benar, itu mubazir, tapi aku hanya ingin tahu mengapa pengenalan mendadak itu opsional? Tampaknya merusak konsistensi sintaks bahasa. Jika saya tidak memiliki kurung kurawal buka untuk menunjukkan blok penginisialisasi, maka ini seharusnya sintaks ilegal. Lucu Anda menyebut Tuan Lippert, saya semacam memancing di depan umum untuk jawabannya sehingga diri saya dan orang lain akan mendapatkan keuntungan dari rasa ingin tahu yang tidak berguna. :)
James Dunne