Saat pemrograman di C #, saya menemukan keputusan desain bahasa yang aneh yang tidak bisa saya mengerti.
Jadi, C # (dan CLR) memiliki dua tipe data agregat: struct
( tipe nilai, disimpan di tumpukan, tidak ada warisan) dan class
(tipe referensi, disimpan di heap, memiliki warisan).
Pengaturan ini terdengar bagus pada awalnya, tetapi kemudian Anda menemukan metode yang mengambil parameter tipe agregat, dan untuk mengetahui apakah itu sebenarnya dari tipe nilai atau tipe referensi, Anda harus menemukan deklarasi tipenya. Kadang-kadang bisa sangat membingungkan.
Solusi yang diterima secara umum untuk masalah ini tampaknya menyatakan semua struct
"tidak berubah" (mengatur bidangnya readonly
) untuk mencegah kemungkinan kesalahan, membatasi struct
kegunaannya.
C ++, misalnya, menggunakan model yang jauh lebih berguna: memungkinkan Anda untuk membuat instance objek baik pada stack atau pada heap dan meneruskannya dengan nilai atau dengan referensi (atau dengan pointer). Saya terus mendengar bahwa C # diinspirasi oleh C ++, dan saya tidak mengerti mengapa tidak menggunakan teknik yang satu ini. Menggabungkan class
dan struct
menjadi satu konstruksi dengan dua opsi alokasi yang berbeda (tumpukan dan tumpukan) dan meneruskannya sebagai nilai atau (secara eksplisit) sebagai referensi melalui ref
dan out
kata kunci sepertinya hal yang baik.
Pertanyaannya adalah, mengapa class
dan struct
menjadi konsep terpisah dalam C # dan CLR bukannya satu jenis agregat dengan dua opsi alokasi?
sumber
struct
s tidak selalu disimpan di tumpukan; pertimbangkan objek denganstruct
bidang. Selain itu, seperti Mason Wheeler menyebutkan masalah mengiris mungkin adalah alasan terbesar.Jawaban:
Alasan C # (dan Java dan pada dasarnya setiap bahasa OO lainnya dikembangkan setelah C ++) tidak menyalin model C ++ dalam aspek ini adalah karena cara C ++ melakukannya adalah kekacauan yang mengerikan.
Anda mengidentifikasi poin yang relevan di atas dengan benar::
struct
jenis nilai, tidak ada warisan.class
: tipe referensi, memiliki warisan. Jenis-jenis warisan dan nilai (atau lebih khusus lagi, polimorfisme dan nilai demi nilai) tidak bercampur; jika Anda meneruskan objek tipeDerived
ke metode argumen tipeBase
, dan kemudian memanggil metode virtual di atasnya, satu-satunya cara untuk mendapatkan perilaku yang tepat adalah untuk memastikan bahwa apa yang diteruskan adalah referensi.Antara itu dan semua kekacauan lain yang Anda temui di C ++ dengan memiliki objek yang dapat diwarisi sebagai tipe nilai (copy constructor dan pengiris objek muncul di pikiran!) Solusi terbaik adalah dengan Just Say No.
Desain bahasa yang baik bukan hanya mengimplementasikan fitur, tetapi juga mengetahui fitur apa yang tidak diimplementasikan, dan salah satu cara terbaik untuk melakukannya adalah dengan belajar dari kesalahan orang-orang yang datang sebelum Anda.
sumber
Dengan analogi, C # pada dasarnya seperti seperangkat alat mekanik di mana seseorang telah membaca bahwa Anda biasanya harus menghindari tang dan kunci pas yang dapat disetel, jadi itu tidak termasuk kunci pas yang bisa disetel sama sekali, dan tang dikunci dalam laci khusus bertanda "tidak aman" , dan hanya dapat digunakan dengan persetujuan dari penyelia, setelah menandatangani penafian yang membebaskan majikan Anda dari segala tanggung jawab atas kesehatan Anda.
C ++, sebagai perbandingan, tidak hanya mencakup kunci pas dan tang yang dapat disesuaikan, tetapi beberapa alat tujuan khusus yang agak aneh yang tujuannya tidak segera terlihat, dan jika Anda tidak tahu cara yang tepat untuk memegangnya, mereka mungkin dengan mudah memotong Anda jempol (tapi begitu Anda mengerti cara menggunakannya, dapat melakukan hal-hal yang pada dasarnya tidak mungkin dengan alat dasar di C # toolbox). Selain itu, ia memiliki mesin bubut, mesin penggilingan, penggiling permukaan, gergaji pita logam, dll., Untuk memungkinkan Anda merancang dan membuat alat yang sama sekali baru setiap kali Anda merasa perlu (tapi ya, alat-alat ahli mesin itu dapat dan akan menyebabkan cedera serius jika Anda tidak tahu apa yang Anda lakukan dengan mereka - atau bahkan jika Anda hanya ceroboh).
Itu mencerminkan perbedaan mendasar dalam filosofi: C ++ mencoba memberi Anda semua alat yang mungkin Anda butuhkan untuk setiap desain yang Anda inginkan. Hampir tidak ada upaya untuk mengendalikan bagaimana Anda menggunakan alat-alat itu, sehingga juga mudah untuk menggunakannya untuk menghasilkan desain yang hanya berfungsi dengan baik dalam situasi yang jarang, serta desain yang mungkin hanya ide yang buruk dan tidak ada yang tahu situasi di mana mereka cenderung bekerja dengan baik. Secara khusus, banyak hal ini dilakukan dengan memisahkan keputusan desain - bahkan yang dalam praktiknya hampir selalu digabungkan. Akibatnya, ada perbedaan besar antara hanya menulis C ++, dan menulis C ++ dengan baik. Untuk menulis C ++ dengan baik, Anda perlu tahu banyak idiom dan aturan praktis (termasuk aturan praktis tentang seberapa serius untuk mempertimbangkan kembali sebelum melanggar aturan praktis lainnya). Hasil dari, C ++ lebih berorientasi pada kemudahan penggunaan (oleh para ahli) daripada kemudahan belajar. Ada juga (terlalu banyak) keadaan di mana itu tidak terlalu mudah digunakan.
C # melakukan lebih banyak upaya untuk memaksa (atau paling tidak sangat menyarankan) apa yang dianggap oleh para perancang bahasa sebagai praktik desain yang baik. Cukup banyak hal yang dipisahkan dalam C ++ (tetapi biasanya berjalan bersama dalam praktek) secara langsung digabungkan dalam C #. Itu memang memungkinkan untuk kode "tidak aman" untuk mendorong batas sedikit, tapi jujur, tidak banyak.
Hasilnya adalah bahwa di satu sisi ada beberapa desain yang dapat diekspresikan secara langsung di C ++ yang secara substansial clumsier untuk diekspresikan dalam C #. Di sisi lain, itu adalah seluruh banyak lebih mudah untuk belajar C #, dan kemungkinan menghasilkan desain yang benar-benar mengerikan yang tidak akan bekerja untuk situasi Anda (atau mungkin lainnya) yang drastis berkurang. Dalam banyak (mungkin bahkan sebagian besar) kasus, Anda bisa mendapatkan desain yang solid dan bisa diterapkan hanya dengan "mengikuti arus", begitulah. Atau, sebagai salah satu teman saya (setidaknya saya suka menganggapnya sebagai teman - tidak yakin apakah dia benar-benar setuju) suka mengatakannya, C # membuatnya mudah untuk jatuh ke dalam jurang kesuksesan.
Jadi melihat lebih spesifik pada pertanyaan tentang bagaimana
class
danstruct
mendapatkan bagaimana mereka berada dalam dua bahasa: objek yang dibuat dalam hierarki warisan di mana Anda dapat menggunakan objek dari kelas turunan dengan kedok kelas dasar / antarmuka, Anda cukup banyak terjebak dengan fakta bahwa Anda biasanya perlu melakukannya melalui semacam pointer atau referensi - pada tingkat yang konkret, yang terjadi adalah bahwa objek dari kelas turunannya mengandung sesuatu memori yang dapat diperlakukan sebagai turunan dari kelas dasar / antarmuka, dan objek yang diturunkan dimanipulasi melalui alamat bagian memori itu.Dalam C ++, terserah kepada programmer untuk melakukannya dengan benar - ketika dia menggunakan warisan, terserah padanya untuk memastikan bahwa (misalnya) fungsi yang bekerja dengan kelas polimorfik dalam suatu hierarki melakukannya melalui pointer atau referensi ke basis kelas.
Dalam C #, apa yang secara fundamental pemisahan yang sama antara jenis jauh lebih eksplisit, dan ditegakkan oleh bahasa itu sendiri. Programmer tidak perlu mengambil langkah apa pun untuk melewati instance kelas dengan referensi, karena itu akan terjadi secara default.
sumber
Ini dari "C #: Mengapa Kita Membutuhkan Bahasa Lain?" - Gunnerson, Eric:
Referensi semantik untuk objek adalah cara untuk menghindari banyak masalah (tentu saja dan tidak hanya mengiris objek) tetapi masalah dunia nyata kadang-kadang membutuhkan objek dengan nilai semantik (mis. Lihat Sounds seperti saya tidak boleh menggunakan referensi semantik, kan? untuk sudut pandang yang berbeda).
Karena itu, pendekatan apa yang lebih baik untuk dilakukan selain memisahkan benda-benda yang kotor, jelek, dan jelek itu dengan semantik nilai di bawah label
struct
?sumber
int[]
harus dapat dibagikan atau diubah (array dapat berupa salah satu, tetapi umumnya tidak keduanya) akan membantu membuat kode yang salah terlihat salah.Daripada memikirkan tipe nilai yang diturunkan
Object
, akan lebih membantu untuk memikirkan tipe lokasi penyimpanan yang ada di alam semesta yang sepenuhnya terpisah dari tipe instance kelas, tetapi untuk setiap tipe nilai memiliki tipe objek-heap yang sesuai. Lokasi penyimpanan tipe struktur hanya menyimpan gabungan bidang publik dan pribadi tipe itu, dan tipe tumpukan dihasilkan secara otomatis sesuai dengan pola seperti:dan untuk pernyataan seperti: Console.WriteLine ("Nilainya adalah {0}", somePoint);
untuk diterjemahkan sebagai: boxed_Point box1 = new boxed_Point (somePoint); Console.WriteLine ("Nilainya adalah {0}", box1);
Dalam praktiknya, karena tipe lokasi penyimpanan dan tipe instance heap ada di alam semesta terpisah, tidak perlu memanggil tipe heap-instance yang seperti
boxed_Int32
: karena sistem akan tahu konteks apa yang membutuhkan instance heap-object dan mana yang membutuhkan lokasi penyimpanan.Beberapa orang berpikir bahwa tipe nilai apa pun yang tidak berperilaku seperti objek harus dianggap "jahat". Saya mengambil pandangan sebaliknya: karena lokasi penyimpanan tipe nilai bukan objek atau referensi ke objek, harapan bahwa mereka harus berperilaku seperti objek harus dianggap tidak membantu. Dalam kasus di mana struct dapat berperilaku seperti objek, tidak ada yang salah dengan memiliki satu melakukannya, tetapi masing
struct
- masing pada intinya tidak lebih dari agregasi bidang publik dan swasta terjebak bersama dengan lakban.sumber