Mengapa struct dan kelas memisahkan konsep dalam C #?

44

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 structkegunaannya.

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 classdan structmenjadi satu konstruksi dengan dua opsi alokasi yang berbeda (tumpukan dan tumpukan) dan meneruskannya sebagai nilai atau (secara eksplisit) sebagai referensi melalui refdan outkata kunci sepertinya hal yang baik.

Pertanyaannya adalah, mengapa classdan structmenjadi konsep terpisah dalam C # dan CLR bukannya satu jenis agregat dengan dua opsi alokasi?

Mints97
sumber
34
"Solusi yang diterima secara umum untuk masalah ini tampaknya menyatakan semua struct sebagai" tidak dapat diubah "... membatasi kegunaan struct '. Banyak orang akan berpendapat bahwa membuat sesuatu yang tidak dapat diubah umumnya membuatnya lebih berguna kapan pun itu bukan karena hambatan kinerja. Juga, structs tidak selalu disimpan di tumpukan; pertimbangkan objek dengan structbidang. Selain itu, seperti Mason Wheeler menyebutkan masalah mengiris mungkin adalah alasan terbesar.
Doval
7
Tidak benar bahwa C # diinspirasi oleh C ++; bukan C # terinspirasi oleh semua (baik bermaksud dan terdengar bagus saat itu) kesalahan dalam desain C ++ dan Java.
Pieter Geerkens
18
Catatan: stack dan heap adalah detail implementasi. Tidak ada yang mengatakan bahwa instance struct harus dialokasikan pada stack dan instance kelas harus dialokasikan pada heap. Dan itu sebenarnya bahkan tidak benar. Sebagai contoh, sangat mungkin bahwa kompiler dapat menentukan menggunakan Escape Analysis bahwa variabel tidak dapat keluar dari cakupan lokal dan dengan demikian mengalokasikannya di stack bahkan jika itu adalah instance kelas. Bahkan tidak mengatakan bahwa harus ada tumpukan atau tumpukan sama sekali. Anda dapat mengalokasikan bingkai lingkungan sebagai daftar tertaut di heap dan bahkan tidak memiliki tumpukan sama sekali.
Jörg W Mittag
3
"tapi kemudian kamu menemukan metode yang mengambil parameter dari tipe agregat, dan untuk mengetahui apakah itu sebenarnya tipe nilai atau tipe referensi, kamu harus menemukan deklarasi tipenya" Umm, mengapa itu penting, tepatnya ? Metode meminta Anda untuk memberikan nilai. Anda memberikan nilai. Pada titik mana Anda harus peduli apakah itu tipe referensi atau tipe nilai?
Luaan

Jawaban:

58

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:: structjenis 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 tipe Derivedke metode argumen tipe Base, 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.

Mason Wheeler
sumber
29
Ini hanyalah kata-kata kasar subjektif yang tidak berguna pada C ++. Tidak bisa downvote, tetapi akan jika saya bisa.
Bartek Banachewicz
17
@MasonWheeler: " berantakan sekali " terdengar cukup subjektif. Ini sudah dibahas dalam utas komentar panjang untuk jawaban Anda yang lain ; utasnya nuked (sayangnya, karena berisi komentar yang berguna meskipun dalam saus perang api) Saya tidak berpikir itu layak mengulangi semuanya di sini, tetapi "C # sudah benar dan C ++ salah" (yang tampaknya menjadi pesan yang Anda coba sampaikan) memang pernyataan yang subjektif.
Andy Prowl
15
@MasonWheeler: Saya lakukan di utas yang mendapat nuked, dan begitu juga beberapa orang lain - itulah sebabnya saya pikir itu disayangkan bahwa itu bisa dihapus. Saya tidak berpikir itu adalah ide yang baik untuk mereplikasi utas itu di sini, tetapi versi singkatnya adalah: di C ++ pengguna jenis itu, bukan perancangnya , yang harus memutuskan dengan semantik apa yang harus digunakan oleh suatu jenis (referensi semantik atau nilai semantik). Ini memiliki kelebihan dan kekurangan: Anda mengamuk melawan kontra tanpa mempertimbangkan (atau mengetahui?) Pro. Itu sebabnya analisisnya subyektif.
Andy Prowl
7
sigh Saya yakin diskusi pelanggaran LSP telah terjadi. Dan saya agak percaya bahwa sebagian besar orang setuju bahwa penyebutan LSP cukup aneh dan tidak terkait, tetapi tidak dapat memeriksa karena mod nuked utas komentar .
Bartek Banachewicz
9
Jika Anda memindahkan paragraf terakhir ke atas dan menghapus paragraf pertama saat ini saya pikir Anda memiliki argumen yang sempurna. Tetapi paragraf pertama saat ini hanya subyektif.
Martin York
19

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 classdan structmendapatkan 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.

Jerry Coffin
sumber
2
Sebagai penggemar C ++, saya pikir ini adalah ringkasan yang sangat baik dari perbedaan antara C # dan Swiss Army Chainsaw.
David Thornley
1
@ Davidvidhorn: Saya setidaknya mencoba untuk menulis apa yang saya pikir akan menjadi perbandingan yang agak seimbang. Bukan untuk menunjukkan jari, tetapi beberapa dari apa yang saya lihat ketika saya menulis ini menurut saya agak ... tidak akurat (dengan kata lain).
Jerry Coffin
7

Ini dari "C #: Mengapa Kita Membutuhkan Bahasa Lain?" - Gunnerson, Eric:

Kesederhanaan adalah tujuan desain yang penting untuk C #.

Dimungkinkan untuk berlebihan dalam kesederhanaan dan kemurnian bahasa, tetapi kemurnian demi kemurnian tidak banyak berguna bagi programmer profesional. Karena itu kami mencoba untuk menyeimbangkan keinginan kami untuk memiliki bahasa yang sederhana dan ringkas dengan memecahkan masalah dunia nyata yang dihadapi oleh programmer.

[...]

Jenis nilai , overloading operator, dan konversi yang ditentukan pengguna semuanya menambah kompleksitas pada bahasa , tetapi memungkinkan skenario pengguna yang penting menjadi sangat disederhanakan.

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?

manlio
sumber
1
Saya tidak tahu, mungkin tidak menggunakan benda-benda semantik yang kotor, jelek, dan buruk itu?
Bartek Banachewicz
Mungkin ... aku adalah orang yang tersesat.
manlio
2
IMHO, salah satu cacat terbesar dalam desain Java adalah tidak adanya sarana untuk menyatakan apakah suatu variabel digunakan untuk merangkum identitas atau kepemilikan , dan salah satu cacat terbesar dalam C # adalah kurangnya sarana untuk membedakan operasi pada variabel dari operasi pada objek yang variabel memegang referensi. Bahkan jika Runtime tidak peduli dengan perbedaan seperti itu, dapat menentukan dalam bahasa apakah variabel tipe int[]harus dapat dibagikan atau diubah (array dapat berupa salah satu, tetapi umumnya tidak keduanya) akan membantu membuat kode yang salah terlihat salah.
supercat
4

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:

// Defined structure
struct Point : IEquatable<Point>
{
  public int X,Y;
  public Point(int x, int y) { X=x; Y=y; }
  public bool Equals(Point other) { return X==other.X && y==other.Y; }
  public bool Equals(Object other)
  { return other != null && other.GetType()==typeof(this) && Equals(Point(other)); }
  public bool ToString() { return String.Format("[{0},{1}", x, y); }
  public bool GetHashCode() { return unchecked(x+y*65531); }
}        
// Auto-generated class
class boxed_Point: IEquatable<Point>
{
  public Point value; // Fake name; C++/CLI, though not C#, allow full access
  public boxed_Point(Point v) { value=v; }
  // Members chain to each member of the original
  public bool Equals(Point other) { return value.Equals(other); }
  public bool Equals(Object other) { return value.Equals(other); }
  public String ToString() { return value.ToString(); }
  public Int32 GetHashCode() { return value.GetHashCode(); }
}

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.

supercat
sumber