Mengapa ini OK dan sebagian besar diharapkan:
abstract type Shape
{
abstract number Area();
}
concrete type Triangle : Shape
{
concrete number Area()
{
//...
}
}
... sementara ini tidak baik dan tidak ada yang mengeluh:
concrete type Name : string
{
}
concrete type Index : int
{
}
concrete type Quantity : int
{
}
Motivasi saya adalah memaksimalkan penggunaan sistem tipe untuk verifikasi ketepatan waktu kompilasi.
PS: ya, saya sudah baca ini dan pembungkusnya sudah beres-beres.
Jawaban:
Saya menganggap Anda berpikir tentang bahasa seperti Java dan C #?
Dalam bahasa-bahasa itu primitif (seperti
int
) pada dasarnya adalah kompromi untuk kinerja. Mereka tidak mendukung semua fitur objek, tetapi mereka lebih cepat dan dengan lebih sedikit overhead.Agar objek mendukung warisan, setiap instance perlu "tahu" pada saat runtime kelas mana itu merupakan instance. Jika tidak, metode yang diganti tidak dapat diselesaikan pada saat runtime. Untuk objek, ini berarti instance data disimpan dalam memori bersama dengan pointer ke objek kelas. Jika info tersebut juga harus disimpan bersama dengan nilai-nilai primitif, persyaratan memori akan membengkak. Nilai integer 16 bit akan membutuhkan 16 bit untuk nilai dan tambahan 32 atau 64 bit memori untuk pointer ke kelasnya.
Terlepas dari overhead memori, Anda juga diharapkan dapat mengesampingkan operasi umum pada primitif seperti operator aritmatika. Tanpa subtyping, operator seperti
+
dapat dikompilasi ke instruksi kode mesin sederhana. Jika itu bisa diganti, Anda harus menyelesaikan metode saat runtime, operasi yang jauh lebih mahal. (Anda mungkin tahu bahwa C # mendukung overloading operator - tetapi ini tidak sama. Overloading operator diselesaikan pada waktu kompilasi, sehingga tidak ada penalti runtime default.)String bukan primitif tetapi mereka masih "istimewa" dalam bagaimana mereka diwakili dalam memori. Misalnya mereka "diinternir", yang berarti dua string literal yang sama dapat dioptimalkan ke referensi yang sama. Ini tidak akan mungkin (atau paling tidak banyak kurang efektif) jika instance string juga harus melacak kelas.
Apa yang Anda uraikan tentu akan bermanfaat, tetapi mendukungnya akan membutuhkan overhead kinerja untuk setiap penggunaan primitif dan string, bahkan ketika mereka tidak memanfaatkan warisan.
Bahasa Smalltalk tidak (saya percaya) memungkinkan subclassing bilangan bulat. Tetapi ketika Java dirancang, Smalltalk dianggap terlalu lambat, dan biaya overhead untuk menjadikan segala sesuatu sebagai objek dianggap sebagai salah satu alasan utama. Java mengorbankan keanggunan dan kemurnian konseptual untuk mendapatkan kinerja yang lebih baik.
sumber
string
disegel karena dirancang untuk berperilaku tidak berubah. Jika seseorang dapat mewarisi dari string, itu mungkin untuk membuat string yang bisa berubah, yang akan membuatnya benar-benar rentan kesalahan. Banyak kode, termasuk kerangka. NET itu sendiri, bergantung pada string yang tidak memiliki efek samping. Lihat juga di sini, memberi tahu Anda hal yang sama: quora.com/Why-String-class-in-C-is-a-sealed-classString
ditandaifinal
di Jawa juga.string
dalam C♯ hanya pasir? Tidak, tentu saja tidak, astring
dalam C♯ adalah apa yang menurut spesifikasi C♯ itu. Bagaimana penerapannya tidak relevan. Implementasi asli C♯ akan mengimplementasikan string sebagai byte array, implementasi ECMAScript akan memetakannya ke ECMAScriptString
s, dll.Apa yang diusulkan beberapa bahasa bukan subklas, tetapi subtipe . Misalnya, Ada memungkinkan Anda membuat jenis turunan atau subtipe . Bagian Ada Pemrograman / Jenis Sistem layak dibaca untuk memahami semua detail. Anda dapat membatasi rentang nilai, yang paling sering Anda inginkan:
Anda dapat menggunakan kedua tipe ini sebagai Integer jika Anda mengonversinya secara eksplisit. Perhatikan juga bahwa Anda tidak dapat menggunakan satu di tempat yang lain, bahkan ketika rentang secara struktural setara (jenis diperiksa dengan nama).
Tipe di atas tidak kompatibel, meskipun mereka mewakili kisaran nilai yang sama.
(Tetapi Anda dapat menggunakan Unchecked_Conversion; jangan beri tahu orang yang saya katakan sebelumnya)
sumber
type Index is -MAXINT..MAXINT;
yang entah bagaimana tidak melakukan apa pun untuk saya karena semua bilangan bulat akan valid? Jadi kesalahan apa yang akan saya dapatkan jika melewati Angle ke Indeks jika semua yang diperiksa adalah rentang?Saya pikir ini mungkin pertanyaan X / Y. Poin yang menonjol, dari pertanyaan ...
... dan dari komentar Anda yang diuraikan:
Maafkan saya jika saya kehilangan sesuatu, tapi ... Jika ini tujuan Anda, lalu mengapa Anda berbicara tentang warisan? Pengganti tersirat adalah ... seperti ... semuanya. Anda tahu, Prinsip Pergantian Liskov?
Apa yang tampaknya Anda inginkan, pada kenyataannya, adalah konsep 'typedef kuat' - di mana sesuatu 'adalah' misalnya
int
dalam hal jangkauan dan representasi tetapi tidak dapat digantikan ke dalam konteks yang mengharapkanint
dan sebaliknya. Saya sarankan mencari info tentang istilah ini dan apa pun bahasa yang Anda pilih menyebutnya. Sekali lagi, secara harfiah hal ini merupakan kebalikan dari warisan.Dan bagi mereka yang mungkin tidak menyukai jawaban X / Y, saya pikir judulnya mungkin masih dapat dijawab dengan mengacu pada LSP. Tipe primitif adalah primitif karena mereka melakukan sesuatu yang sangat sederhana, dan hanya itu yang mereka lakukan . Membiarkan mereka diwarisi dan dengan demikian membuat efek tak terbatas yang mungkin terjadi akan menghasilkan kejutan besar pada pelanggaran LSP fatal dan paling fatal. Jika saya bisa dengan optimis menganggap Thales Pereira tidak keberatan saya mengutip komentar fenomenal ini :
Jika seseorang melihat tipe primitif, dalam bahasa yang waras, mereka dengan benar mengira itu akan selalu melakukan satu hal kecil, sangat baik, tanpa kejutan. Tipe primitif tidak memiliki deklarasi kelas yang tersedia yang menandakan apakah mereka mungkin diwariskan atau tidak dan metode mereka diganti. Jika ya, tentu akan sangat mengejutkan (dan benar-benar merusak kompatibilitas, tapi saya sadar itu jawaban mundur untuk 'mengapa X tidak dirancang dengan Y').
... meskipun, seperti yang ditunjukkan Mooing Duck dalam respons, bahasa yang memungkinkan overloading operator memungkinkan pengguna untuk mengacaukan diri mereka sendiri pada tingkat yang sama atau sama jika mereka benar-benar menginginkannya, sehingga meragukan apakah argumen terakhir ini berlaku. Dan saya akan berhenti meringkas komentar orang lain sekarang, heh.
sumber
Untuk memungkinkan pewarisan dengan pengiriman virtual (yang sering dianggap cukup diinginkan dalam desain aplikasi), seseorang perlu informasi jenis runtime. Untuk setiap objek, beberapa data mengenai jenis objek harus disimpan. Primitif, per definisi, tidak memiliki informasi ini.
Ada dua (dikelola, dijalankan pada VM) bahasa OOP utama yang fitur primitif: C # dan Java. Banyak bahasa lain tidak memiliki primitif di tempat pertama, atau menggunakan alasan yang sama untuk memungkinkan mereka / menggunakannya.
Primitif adalah kompromi untuk kinerja. Untuk setiap objek, Anda memerlukan ruang untuk header objeknya (Di Jawa, biasanya 2 * 8 byte pada VM 64-bit), ditambah bidangnya, ditambah padding akhirnya (Dalam Hotspot, setiap objek menempati sejumlah byte yang merupakan kelipatan dari 8). Jadi
int
objek sebagai akan membutuhkan setidaknya 24 byte memori yang harus disimpan, bukan hanya 4 byte (di Jawa).Dengan demikian, tipe primitif ditambahkan untuk meningkatkan kinerja. Mereka membuat banyak hal lebih mudah. Apa
a + b
artinya jika keduanya merupakan subtipeint
? Beberapa jenis dispathcing harus ditambahkan untuk memilih tambahan yang benar. Ini berarti pengiriman virtual. Memiliki kemampuan untuk menggunakan opcode yang sangat sederhana untuk penambahan jauh, jauh lebih cepat, dan memungkinkan untuk optimasi waktu kompilasi.String
adalah kasus lain. Baik dalam Java dan C #,String
adalah sebuah objek. Tapi di C # disegel, dan di Jawa final. Itu karena kedua pustaka standar Java dan C # memerlukanString
s agar tidak dapat diubah, dan mensubklasifikasikan mereka akan memecah kekekalan ini.Dalam kasus Java, VM dapat (dan tidak) menginternir Strings dan "menyatukan" mereka, memungkinkan untuk kinerja yang lebih baik. Ini hanya berfungsi ketika String benar-benar tidak dapat diubah.
Plus, seseorang jarang perlu mensubklasifikasikan tipe primitif. Selama primitif tidak dapat disubklasifikasikan, ada banyak hal rapi yang dikatakan matematika tentang mereka. Misalnya, kita dapat memastikan bahwa penambahan bersifat komutatif dan asosiatif. Itulah sesuatu yang dikatakan definisi matematika bilangan bulat. Lebih jauh lagi, kita dapat dengan mudah melakukan invarian invariant melalui loop melalui induksi dalam banyak kasus. Jika kita mengizinkan subklasifikasi
int
, kita kehilangan alat yang diberikan matematika, karena kita tidak lagi dapat dijamin bahwa properti tertentu berlaku. Jadi, saya akan mengatakan kemampuan tidak dapat subclass tipe primitif sebenarnya hal yang baik. Lebih sedikit hal yang dapat dipecahkan seseorang, plus kompiler sering dapat membuktikan bahwa ia diizinkan melakukan optimasi tertentu.sumber
to allow inheritance, one needs runtime type information.
Salah.For every object, some data regarding the type of the object has to be stored.
Salah.There are two mainstream OOP languages that feature primitives: C# and Java.
Apa, apakah C ++ bukan arus utama sekarang? Saya akan menggunakannya sebagai bantahan saya karena informasi jenis runtime adalah istilah C ++. Sama sekali tidak diperlukan kecuali menggunakandynamic_cast
atautypeid
. Dan bahkan jika RTTI aktif, pewarisan hanya menghabiskan ruang jika suatu kelas memilikivirtual
metode yang tabel per kelasnya harus ditunjukkan per instancedynamic_cast
dantypeinfo
membutuhkan itu. Pengiriman virtual secara praktis diimplementasikan menggunakan pointer ke vtable untuk kelas konkret objek, sehingga membiarkan fungsi yang tepat dipanggil, tetapi tidak memerlukan detail jenis dan hubungan yang melekat dalam RTTI. Yang perlu diketahui oleh kompiler adalah apakah kelas objek polimorfik dan, jika demikian, apa vptr instance itu. Seseorang dapat dengan mudah mengkompilasi kelas yang sebenarnya dikirim-fno-rtti
.dynamic_cast
pada kelas tanpa pengiriman virtual. Alasan implementasi adalah bahwa RTTI umumnya diimplementasikan sebagai anggota tersembunyi dari suatu tabel.Dalam bahasa OOP statis statis yang kuat, sub-pengetikan dilihat terutama sebagai cara untuk memperluas jenis dan untuk menimpa metode jenis saat ini.
Untuk melakukannya, 'objek' berisi pointer ke tipenya. Ini adalah overhead: kode dalam metode yang menggunakan
Shape
instance harus terlebih dahulu mengakses informasi jenis instance itu, sebelum ia mengetahuiArea()
metode yang benar untuk dipanggil.Primitif cenderung hanya memungkinkan operasi di atasnya yang dapat diterjemahkan ke dalam instruksi bahasa mesin tunggal dan tidak membawa informasi jenis apa pun dengannya. Membuat bilangan bulat lebih lambat sehingga seseorang dapat mensubklasnya menjadi tidak menarik untuk menghentikan bahasa apa pun yang menjadi arus utama.
Jadi jawaban untuk:
Aku s:
Namun, kami mulai mendapatkan bahasa yang memungkinkan pemeriksaan statis berdasarkan properti variabel selain 'tipe', misalnya F # memiliki "dimensi" dan "unit" sehingga Anda tidak dapat, misalnya, menambahkan panjang ke area .
Ada juga bahasa yang memungkinkan 'tipe yang ditentukan pengguna' yang tidak mengubah (atau menukar) apa yang dilakukan suatu tipe, tetapi hanya membantu dengan pemeriksaan tipe statis; lihat jawaban coredump.
sumber
Saya tidak yakin apakah saya menghadap sesuatu di sini, tetapi jawabannya agak sederhana:
Perhatikan bahwa hanya ada dua bahasa OOP statis yang kuat yang bahkan memiliki primitif, AFAIK: Java dan C ++. (Sebenarnya, saya bahkan tidak yakin tentang yang terakhir, saya tidak tahu banyak tentang C ++, dan apa yang saya temukan saat mencari membingungkan.)
Dalam C ++, primitif pada dasarnya adalah warisan yang diwarisi (pun intended) dari C. Jadi, mereka tidak mengambil bagian dalam sistem objek (dan dengan demikian warisan) karena C tidak memiliki sistem objek atau warisan.
Di Jawa, primitif adalah hasil dari upaya sesat untuk meningkatkan kinerja. Primitif juga merupakan satu-satunya tipe nilai dalam sistem, pada kenyataannya, tidak mungkin untuk menulis tipe nilai di Jawa, dan tidak mungkin bagi objek untuk menjadi tipe nilai. Jadi, terlepas dari fakta bahwa primitif tidak mengambil bagian dalam sistem objek dan dengan demikian gagasan "warisan" bahkan tidak masuk akal, bahkan jika Anda dapat mewarisinya, Anda tidak akan dapat mempertahankan " nilai-ness ". Ini berbeda dari misalnya C♯ yang memang memiliki tipe nilai
struct
, yang tetap merupakan objek.Hal lain adalah bahwa tidak dapat mewarisi sebenarnya juga tidak unik untuk orang primitif. Dalam C♯,
struct
s secara implisit mewarisi dariSystem.Object
dan dapat mengimplementasikaninterface
s, tetapi mereka tidak dapat mewarisi dari atau diwarisi olehclass
es ataustruct
s. Juga,sealed
class
es tidak dapat diwarisi dari. Di Jawa,final
class
es tidak dapat diwarisi dari.tl; dr :
final
atausealed
di Jawa atau C♯,struct
di C♯,case class
es di Scala)sumber
virtual
, yang berarti mereka tidak mematuhi LSP. Misalnyastd::string
bukan primitif, tetapi sangat berperilaku sebagai nilai lain. Semantik nilai semacam itu cukup umum, seluruh bagian STL dari C ++ mengasumsikannya.int
Anda gunakan. Setiap alokasi mengambil urutan 100ns ditambah overhead pengumpulan sampah. Bandingkan dengan siklus CPU tunggal yang dikonsumsi dengan menambahkan dua primitifint
. Kode java Anda akan merangkak jika perancang bahasa telah memutuskan sebaliknya.int
, sehingga mereka melakukan hal yang persis sama. (Scala-asli mengkompilasi mereka ke dalam register mesin primitif, Scala.js mengkompilasi mereka ke dalam ECMAScriptNumber
s primitif .) Ruby tidak memiliki primitif, tetapi YARV dan Rubinius mengkompilasi bilangan bulat menjadi bilangan bulat mesin primitif, JRuby mengkompilasi mereka ke dalam primitif JVMlong
. Hampir setiap implementasi Lisp, Smalltalk, atau Ruby menggunakan primitif di VM . Di situlah optimalisasi kinerja…Joshua Bloch dalam "Java Efektif" merekomendasikan desain secara eksplisit untuk warisan atau melarangnya. Kelas primitif tidak dirancang untuk pewarisan karena mereka dirancang untuk tidak berubah dan memungkinkan pewarisan dapat mengubah itu dalam subkelas, sehingga melanggar prinsip Liskov dan itu akan menjadi sumber banyak bug.
Ngomong-ngomong, mengapa ini solusi hacky? Anda harus benar-benar lebih suka komposisi daripada warisan. Jika alasannya adalah kinerja daripada yang ada benarnya dan jawaban untuk pertanyaan Anda adalah bahwa tidak mungkin untuk menempatkan semua fitur di Jawa karena butuh waktu untuk menganalisis semua aspek yang berbeda dalam menambahkan fitur. Misalnya Java tidak memiliki Generics sebelum 1.5.
Jika Anda memiliki banyak kesabaran maka Anda beruntung karena ada rencana untuk menambahkan kelas nilai ke Jawa yang akan memungkinkan Anda untuk membuat kelas nilai Anda yang akan membantu Anda meningkatkan kinerja dan pada saat yang sama akan memberi Anda lebih banyak fleksibilitas.
sumber
Pada tingkat abstrak, Anda dapat memasukkan apa pun yang Anda inginkan dalam bahasa yang Anda desain.
Pada level implementasi, tidak terhindarkan bahwa beberapa dari hal-hal itu akan lebih mudah untuk diimplementasikan, beberapa akan rumit, beberapa dapat dibuat cepat, beberapa pasti akan lebih lambat, dan sebagainya. Untuk menjelaskan hal ini, desainer sering harus membuat keputusan dan kompromi yang sulit.
Pada tingkat implementasi, salah satu cara tercepat yang kami lakukan untuk mengakses suatu variabel adalah dengan mengetahui alamatnya dan memuat konten dari alamat itu. Ada instruksi khusus di sebagian besar CPU untuk memuat data dari alamat dan instruksi itu biasanya perlu tahu berapa byte yang mereka perlu muat (satu, dua, empat, delapan, dll) dan di mana harus meletakkan data yang mereka muat (register tunggal, register pasangan, register yang diperluas, memori lain, dll). Dengan mengetahui ukuran suatu variabel, kompiler dapat mengetahui instruksi mana yang harus dipancarkan untuk penggunaan variabel tersebut. Dengan tidak mengetahui ukuran variabel, kompiler harus menggunakan sesuatu yang lebih rumit dan mungkin lebih lambat.
Pada tingkat abstrak, titik subtipe adalah untuk dapat menggunakan contoh dari satu jenis di mana jenis yang sama atau lebih umum diharapkan. Dengan kata lain, kode dapat ditulis yang mengharapkan objek dari tipe tertentu atau apa pun yang lebih diturunkan, tanpa mengetahui sebelumnya apa sebenarnya ini. Dan jelas, karena lebih banyak tipe turunan dapat menambah lebih banyak anggota data, tipe turunan tidak harus memiliki persyaratan memori yang sama dengan tipe dasarnya.
Pada tingkat implementasi, tidak ada cara sederhana untuk variabel ukuran yang telah ditentukan untuk menyimpan instance ukuran yang tidak diketahui dan diakses dengan cara yang biasa Anda sebut efisien. Tetapi ada cara untuk memindahkan barang-barang sekitar sedikit dan menggunakan variabel untuk tidak menyimpan objek, tetapi untuk mengidentifikasi objek dan membiarkan objek itu disimpan di tempat lain. Cara itu adalah referensi (misalnya alamat memori) - tingkat tipuan ekstra yang memastikan bahwa variabel hanya perlu menyimpan beberapa jenis informasi ukuran tetap, selama kita dapat menemukan objek melalui informasi itu. Untuk mencapai itu, kita hanya perlu memuat alamat (ukuran tetap) dan kemudian kita dapat bekerja seperti biasa menggunakan offset dari objek yang kita tahu valid, bahkan jika objek itu memiliki lebih banyak data di offset yang tidak kita ketahui. Kita bisa melakukan itu karena kita tidak
Pada tingkat abstrak, metode ini memungkinkan Anda untuk menyimpan (referensi ke a)
string
ke dalamobject
variabel tanpa kehilangan informasi yang membuatnya menjadistring
. Tidak masalah bagi semua tipe untuk bekerja seperti ini dan Anda mungkin juga mengatakan itu elegan dalam banyak hal.Namun, pada tingkat implementasi, tingkat tipuan ekstra melibatkan lebih banyak instruksi dan pada kebanyakan arsitektur itu membuat setiap akses ke objek agak lambat. Anda dapat mengizinkan kompilator memeras lebih banyak kinerja dari suatu program jika Anda memasukkan dalam bahasa Anda beberapa tipe yang umum digunakan yang tidak memiliki tingkat tipuan ekstra (referensi). Tetapi dengan menghapus tingkat tipuan itu, kompiler tidak dapat lagi memungkinkan Anda untuk mengetikkan kembali dalam cara yang aman memori. Itu karena jika Anda menambahkan lebih banyak anggota data ke tipe Anda dan Anda menetapkan ke tipe yang lebih umum, setiap anggota data tambahan yang tidak sesuai dengan ruang yang dialokasikan untuk variabel target akan dipotong.
sumber
Secara umum
Jika suatu kelas adalah abstrak (metafora: kotak dengan lubang), tidak apa-apa (bahkan diharuskan untuk memiliki sesuatu yang dapat digunakan!) Untuk "mengisi lubang", itu sebabnya kami mensubkelas kelas abstrak.
Jika suatu kelas adalah konkret (metafora: satu kotak penuh), tidak apa-apa untuk mengubah yang sudah ada karena jika penuh, itu penuh. Kami tidak punya ruang untuk menambahkan sesuatu lagi di dalam kotak, itu sebabnya kami tidak boleh subkelas kelas beton.
Dengan primitif
Primitif adalah kelas beton dengan desain. Mereka mewakili sesuatu yang terkenal, sepenuhnya pasti (saya belum pernah melihat tipe primitif dengan sesuatu yang abstrak, kalau tidak itu bukan primitif lagi) dan banyak digunakan melalui sistem. Mengizinkan untuk mensubklasifikasikan tipe primitif dan memberikan implementasi Anda sendiri kepada orang lain yang bergantung pada perilaku primitif yang dirancang dapat menyebabkan banyak efek samping dan kerusakan besar!
sumber
Biasanya warisan bukan semantik yang Anda inginkan, karena Anda tidak dapat mengganti tipe khusus Anda di mana pun primitif diharapkan. Meminjam dari contoh Anda, a
Quantity + Index
tidak masuk akal secara semantik, jadi hubungan pewarisan adalah hubungan yang salah.Namun, beberapa bahasa memiliki konsep tipe nilai yang mengungkapkan jenis hubungan yang Anda gambarkan. Scala adalah salah satu contohnya. Tipe nilai menggunakan primitif sebagai representasi yang mendasarinya, tetapi memiliki identitas kelas yang berbeda dan operasi di luar. Itu memiliki efek memperluas tipe primitif, tetapi lebih merupakan komposisi daripada hubungan warisan.
sumber