Apa perbedaan antara subkelas dan subtipe?

44

Jawaban dengan nilai tertinggi untuk pertanyaan ini tentang Prinsip Pergantian Liskov bersusah payah untuk membedakan antara istilah subtipe dan subkelas . Ini juga menunjukkan bahwa beberapa bahasa mengacaukan keduanya, sedangkan yang lain tidak.

Untuk bahasa berorientasi objek yang paling saya kenal (Python, C ++), "type" dan "class" adalah konsep yang identik. Dalam hal C ++, apa artinya memiliki perbedaan antara subtipe dan subkelas? Katakanlah, misalnya, itu Fooadalah subkelas, tetapi bukan subtipe dari FooBase. Jika foomerupakan turunan dari Foo, apakah baris ini:

FooBase* fbPoint = &foo;

tidak lagi valid?

tel
sumber
6
Sebenarnya, dalam Python "tipe" dan "kelas" adalah konsep yang berbeda . Faktanya, Python sedang diketik secara dinamis, "type" bukanlah konsep sama sekali dalam Python. Sayangnya, pengembang Python tidak mengerti itu, dan masih mengacaukan keduanya.
Jörg W Mittag
11
"Tipe" dan "kelas" juga berbeda dalam C ++. "Array of ints" adalah tipe; kelas apa itu? "pointer ke variabel tipe int" adalah tipe; kelas apa itu? Hal-hal ini bukan kelas apa pun, tetapi mereka pasti tipe.
Eric Lippert
2
Saya ingin tahu hal ini setelah membaca pertanyaan itu dan jawaban itu.
user369450
4
@JorgWMittag Jika tidak ada konsep "tipe" dalam python maka seseorang harus memberi tahu siapa pun yang menulis dokumentasi: docs.python.org/3/library/stdtypes.html
Matt
@Matt to be fair, tipe-tipe yang dibuat di 3,5 yang sangat baru, terutama oleh apa-yang-saya-diizinkan-untuk-digunakan-dalam-produksi standar.
Jared Smith

Jawaban:

53

Subtyping adalah bentuk polimorfisme tipe di mana subtipe adalah tipe data yang terkait dengan tipe data lain (supertype) oleh beberapa gagasan tentang substitusi, artinya elemen program, biasanya subrutin atau fungsi, ditulis untuk beroperasi pada elemen supertipe juga dapat beroperasi pada elemen subtipe.

Jika Smerupakan subtipe dari T, hubungan subtyping sering ditulis S <: T, yang berarti bahwa setiap istilah tipe Sdapat digunakan dengan aman dalam konteks di mana istilah tipe Tdiharapkan. Semantik yang tepat dari subtyping secara krusial tergantung pada keterangan dari apa yang "aman digunakan dalam konteks di mana" berarti dalam bahasa pemrograman yang diberikan.

Subclassing tidak harus bingung dengan subtyping. Secara umum, subtyping membentuk hubungan is-a, sedangkan subclassing hanya menggunakan kembali implementasi dan membangun hubungan sintaksis, tidak harus hubungan semantik (pewarisan tidak memastikan subtyping perilaku).

Untuk membedakan konsep-konsep ini, subtyping juga dikenal sebagai pewarisan antarmuka , sedangkan subclassing dikenal sebagai warisan implementasi atau pewarisan kode.

Referensi
subtyping
Warisan

Robert Harvey
sumber
1
Kata yang sangat bagus. Mungkin layak disebutkan dalam konteks pertanyaan bahwa programmer C ++ sering menggunakan kelas dasar virtual murni untuk mengkomunikasikan hubungan subtyping ke sistem tipe. Pendekatan pemrograman generik sering lebih disukai tentu saja.
Aluan Haddad
6
"Semantik yang tepat dari subtyping sangat tergantung pada rincian dari apa yang" digunakan dengan aman dalam konteks di mana "berarti dalam bahasa pemrograman yang diberikan." … Dan LSP mendefinisikan gagasan yang agak masuk akal tentang apa yang dimaksud dengan "aman" dan memberi tahu kita batasan apa yang harus dipenuhi oleh spesifikasi tersebut untuk memungkinkan bentuk "keselamatan" tertentu ini.
Jörg W Mittag
Satu lagi sampel di tumpukan: jika saya mengerti dengan benar, di C ++, publicpewarisan memperkenalkan subtipe sementara privatepewarisan memperkenalkan subkelas.
Quentin
@Quentin warisan publik adalah subtipe dan subkelas, tetapi pribadi namun hanya subkelas, tetapi bukan subtipe. Anda dapat memiliki subtyping tanpa subclassing dengan struktur seperti antarmuka Java
menyamakan
27

Suatu tipe , dalam konteks yang kita bicarakan di sini, pada dasarnya adalah seperangkat jaminan perilaku. Sebuah kontrak , jika Anda mau. Atau, meminjam terminologi dari Smalltalk, sebuah protokol .

Sebuah kelas adalah bundel metode. Ini adalah seperangkat implementasi perilaku .

Subtyping adalah cara untuk menyempurnakan protokol. Subclassing adalah cara menggunakan kembali kode diferensial, yaitu menggunakan kembali kode dengan hanya menggambarkan perbedaan dalam perilaku.

Jika Anda telah menggunakan Java atau C♯, maka Anda mungkin menemukan saran bahwa semua tipe harus interfacetipe. Bahkan, jika Anda membaca Abstraksi Data Pemahaman William Cook , Dikunjungi Kembali , maka Anda mungkin tahu bahwa untuk melakukan OO dalam bahasa tersebut, Anda hanya boleh menggunakan interfaces sebagai tipe. (Juga, fakta menyenangkan: Java cribbed interfaces langsung dari protokol Objective-C, yang pada gilirannya diambil langsung dari Smalltalk.)

Sekarang, jika kita mengikuti saran pengkodean ke kesimpulan logis dan membayangkan versi Java, di mana hanya interface tipe, dan kelas dan primitif tidak, maka satu interfacemewarisi dari yang lain akan membuat hubungan subtipe, sedangkan yang classmewarisi dari yang lain akan hanya untuk menggunakan kembali kode diferensial melalui super.

Sejauh yang saya tahu, tidak ada bahasa yang diketik secara statis arus utama yang membedakan secara ketat antara kode pewarisan (implementasi pewarisan / subklasifikasi) dan kontrak pewarisan (subtipe). Di Java dan C♯, pewarisan antarmuka adalah subtyping murni (atau paling tidak, sampai diperkenalkannya metode default di Java 8 dan kemungkinan juga C♯ 8), tetapi pewarisan kelas juga subtipe dan juga pewarisan implementasi. Saya ingat pernah membaca tentang dialek LISP berorientasi objek eksperimental statis, yang secara ketat membedakan antara mixin (yang berisi perilaku), struct (yang berisi keadaan), interface (yang menggambarkanperilaku), dan kelas (yang menyusun nol atau lebih struct dengan satu atau lebih mixin dan sesuai dengan satu atau lebih antarmuka). Hanya kelas yang dapat dipakai, dan hanya antarmuka yang dapat digunakan sebagai tipe.

Dalam bahasa OO yang diketik secara dinamis seperti Python, Ruby, ECMAScript, atau Smalltalk, kita biasanya menganggap jenis objek sebagai sekumpulan protokol yang sesuai dengannya. Perhatikan bentuk jamak: suatu objek dapat memiliki beberapa tipe, dan saya tidak hanya berbicara tentang fakta bahwa setiap objek tipe Stringjuga merupakan objek tipe Object. (BTW: perhatikan bagaimana saya menggunakan nama kelas untuk berbicara tentang tipe? Betapa bodohnya saya!) Suatu objek dapat mengimplementasikan beberapa protokol. Misalnya, di Ruby, Arraysdapat ditambahkan ke, mereka dapat diindeks, mereka dapat diiterasi, dan mereka dapat dibandingkan. Itu empat protokol berbeda yang mereka implementasikan!

Sekarang, Ruby tidak memiliki tipe. Tetapi komunitas Ruby memiliki tipe! Mereka hanya ada di kepala programmer. Dan dalam dokumentasi. Misalnya, objek apa pun yang merespons metode yang disebut eachdengan menghasilkan elemen-elemennya satu per satu dianggap sebagai objek yang dapat dihitung . Dan ada mixin yang dipanggil Enumerableyang tergantung pada protokol ini. Jadi, jika objek Anda memiliki benar tipe (yang hanya ada di kepala programmer), maka diperbolehkan untuk campuran dalam (mewarisi dari) Enumerablemixin, dan juga mendapatkan segala macam metode keren gratis, seperti map, reduce, filterdan sebagainya di.

Demikian juga, jika sebuah objek merespon <=>, maka itu dianggap untuk melaksanakan sebanding protokol, dan dapat mencampur dalam Comparablemixin dan mendapatkan hal-hal seperti <, <=, >, <=, ==, between?, dan clampgratis. Namun, ia juga dapat mengimplementasikan semua metode itu sendiri, dan tidak mewarisi dari Comparablesemuanya, dan itu masih akan dianggap sebanding .

Contoh yang baik adalah StringIOperpustakaan, yang pada dasarnya memalsukan aliran I / O dengan string. Ini mengimplementasikan semua metode yang sama seperti IOkelas, tetapi tidak ada hubungan warisan antara keduanya. Namun demikian, a StringIOdapat digunakan di mana saja dan IOdapat digunakan. Ini sangat berguna dalam unit test, di mana Anda dapat mengganti file atau stdindengan StringIOtanpa harus membuat perubahan lebih lanjut pada program Anda. Karena StringIOsesuai dengan protokol yang sama IO, keduanya memiliki tipe yang sama, meskipun mereka adalah kelas yang berbeda, dan tidak memiliki hubungan (selain dari hal sepele yang mereka berdua kembangkan Objectpada beberapa titik).

Jörg W Mittag
sumber
Mungkin bermanfaat jika bahasa mengizinkan program untuk secara bersamaan mendeklarasikan jenis kelas dan antarmuka yang kelasnya merupakan implementasi, dan juga memungkinkan implementasi untuk menentukan "konstruktor" (yang akan berantai ke konstruktor kelas yang ditentukan oleh antarmuka). Untuk jenis objek yang referensi akan dibagikan secara publik, pola yang disukai adalah bahwa jenis kelas hanya digunakan saat membuat kelas turunan; kebanyakan referensi harus dari tipe antarmuka. Mampu menentukan konstruktor antarmuka akan sangat membantu dalam situasi di mana ...
supercat
... mis. kode membutuhkan koleksi yang akan memungkinkan serangkaian nilai tertentu untuk dibaca oleh indeks, tetapi tidak terlalu peduli apa jenisnya. Meskipun ada alasan kuat untuk mengenali kelas dan antarmuka sebagai jenis tipe yang berbeda, ada banyak situasi di mana mereka harus dapat bekerja lebih dekat bersama daripada bahasa yang saat ini memungkinkan.
supercat
Apakah Anda memiliki referensi atau beberapa kata kunci yang dapat saya cari beberapa informasi lebih lanjut tentang dialek LISP eksperimental yang Anda sebutkan yang secara formal membedakan mixin, struct, antarmuka, dan kelas?
tel
@tel: Tidak, maaf. Itu mungkin sekitar 15-20 tahun yang lalu, dan pada saat itu minat saya ada di mana-mana. Saya tidak mungkin mulai memberi tahu Anda apa yang saya cari ketika saya menemukan ini.
Jörg W Mittag
Awww. Itu adalah detail paling menarik dari semua jawaban ini. Fakta bahwa pemisahan formal dari konsep-konsep itu sebenarnya mungkin dalam implementasi bahasa benar-benar membantu mengkristalisasi perbedaan kelas / tipe untuk saya. Saya kira saya akan pergi mencari LISP itu sendiri, bagaimanapun juga. Apakah Anda ingat jika Anda membacanya di artikel / buku jurnal, atau jika Anda baru saja mendengarnya dalam percakapan?
tel.
2

Mungkin pertama-tama berguna untuk membedakan antara suatu tipe dan kelas dan kemudian menyelami perbedaan antara subtipe dan subklas.

Untuk sisa jawaban ini saya akan berasumsi bahwa tipe dalam diskusi adalah tipe statis (karena subtyping biasanya muncul dalam konteks statis).

Saya akan mengembangkan pseudocode mainan untuk membantu mengilustrasikan perbedaan antara tipe dan kelas karena sebagian besar bahasa mengacaukan mereka setidaknya sebagian (untuk alasan yang baik saya akan menyinggung secara singkat).

Mari kita mulai dengan suatu tipe. Tipe adalah label untuk ekspresi dalam kode Anda. Nilai label ini dan apakah konsisten (untuk beberapa jenis definisi spesifik sistem-spesifik) dengan semua nilai label lain dapat ditentukan oleh program eksternal (pengetik ketik) tanpa menjalankan program Anda. Itulah yang membuat label-label ini istimewa dan pantas untuk namanya sendiri.

Dalam bahasa mainan kami, kami mungkin mengizinkan pembuatan label seperti itu.

declare type Int
declare type String

Maka kita mungkin memberi label berbagai nilai sebagai tipe ini.

0 is of type Int
1 is of type Int
-1 is of type Int
...

"" is of type String
"a" is of type String
"b" is of type String
...

Dengan pernyataan ini, juru ketik kami sekarang dapat menolak pernyataan seperti

0 is of type String

jika salah satu persyaratan dari sistem tipe kami adalah bahwa setiap ekspresi memiliki tipe yang unik.

Mari kita kesampingkan untuk sekarang betapa kikuknya ini dan bagaimana Anda akan mengalami masalah dalam menetapkan jumlah jenis ekspresi yang tak terbatas. Kita bisa kembali lagi nanti.

Kelas di sisi lain adalah kumpulan metode dan bidang yang dikelompokkan bersama (berpotensi dengan pengubah akses seperti pribadi atau publik).

class StringClass:
  defMethod concatenate(otherString): ...
  defField size: ...

Sebuah instance dari kelas ini mendapatkan kemampuan untuk membuat atau menggunakan definisi yang sudah ada sebelumnya dari metode dan bidang ini.

Kita bisa memilih untuk mengaitkan kelas dengan tipe sehingga setiap instance kelas secara otomatis dilabeli dengan tipe itu.

associate StringClass with String

Tetapi tidak setiap jenis perlu memiliki kelas terkait.

# Hmm... Doesn't look like there's a class for Int

Bisa dibayangkan juga bahwa dalam bahasa mainan kita tidak setiap kelas memiliki tipe, terutama jika tidak semua ekspresi kita memiliki tipe. Agak sulit (tapi bukan tidak mungkin) untuk membayangkan seperti apa aturan konsistensi sistem tipe jika beberapa ekspresi memiliki tipe dan beberapa tidak.

Terlebih lagi dalam bahasa mainan kita, asosiasi ini tidak harus unik. Kita bisa mengasosiasikan dua kelas dengan tipe yang sama.

associate MyCustomStringClass with String

Sekarang ingatlah bahwa tidak ada keharusan bagi juru ketik kami untuk melacak nilai ekspresi (dan dalam kebanyakan kasus tidak akan atau tidak mungkin melakukannya). Yang ia tahu hanyalah label yang Anda beri tahu. Sebagai pengingat sebelumnya, typechecker hanya dapat menolak pernyataan itu 0 is of type Stringkarena aturan jenis yang dibuat secara artifisial bahwa ekspresi harus memiliki tipe unik dan kami sudah memberi label ekspresi 0lain. Itu tidak memiliki pengetahuan khusus tentang nilai 0.

Jadi bagaimana dengan subtyping? Subtipe dengan baik adalah nama untuk aturan umum dalam pengetikan yang merelaksasi aturan lain yang mungkin Anda miliki. Yaitu jika A is subtype of Bkemudian di mana-mana typechecker Anda meminta label B, itu juga akan menerima A.

Sebagai contoh, kita dapat melakukan hal berikut untuk nomor kita daripada apa yang kita miliki sebelumnya.

declare type NaturalNum
declare type Int
NaturalNum is subtype of Int

0 is of type NaturalNum
1 is of type NaturalNum
-1 is of type Int
...

Subclassing adalah singkatan untuk mendeklarasikan kelas baru yang memungkinkan Anda untuk menggunakan kembali metode dan bidang yang sebelumnya dinyatakan.

class ExtendedStringClass is subclass of StringClass:
  # We get concatenate and size for free!
  def addQuestionMark: ...

Kami tidak harus mengaitkan contoh ExtendedStringClassdengan Stringseperti yang kami lakukan StringClasssejak, setelah semua itu adalah kelas yang sama sekali baru, kami hanya tidak perlu menulis terlalu banyak. Ini akan memungkinkan kami untuk memberikan ExtendedStringClassjenis yang tidak kompatibel dengan Stringdari sudut pandang pengetik huruf.

Demikian juga kita dapat memutuskan untuk membuat kelas yang baru NewClassdan selesai

associate NewClass with String

Sekarang setiap instance StringClassdapat diganti dengan NewClassdari sudut pandang typechecker.

Jadi dalam teori, subtyping dan subclassing adalah hal yang sangat berbeda. Tapi tidak ada bahasa yang saya tahu yang memiliki tipe dan kelas yang benar-benar melakukan hal-hal seperti ini. Mari kita mulai mengurangi bahasa kita dan menjelaskan alasan di balik beberapa keputusan kita.

Pertama, meskipun dalam teori kelas yang benar-benar berbeda dapat diberikan jenis yang sama atau kelas dapat diberi jenis yang sama dengan nilai-nilai yang bukan contoh dari kelas apa pun, ini sangat menghambat kegunaan dari pengetik huruf. Typechecker dirampas secara efektif dari kemampuan untuk memeriksa apakah metode atau bidang yang Anda panggil dalam ekspresi benar-benar ada pada nilai itu, yang mungkin merupakan cek yang Anda inginkan jika Anda akan kesulitan bermain bersama dengan typechecker. Lagi pula, siapa yang tahu apa sebenarnya nilai di bawah Stringlabel itu; itu mungkin sesuatu yang tidak memiliki, misalnya, concatenatemetode sama sekali!

Oke jadi mari kita menetapkan bahwa setiap kelas secara otomatis menghasilkan tipe baru dengan nama yang sama dengan kelas itu dan associatecontoh dengan tipe itu. Itu memungkinkan kita menyingkirkan associateserta berbagai nama antara StringClassdan String.

Untuk alasan yang sama, kami mungkin ingin secara otomatis membangun hubungan subtipe antara tipe dua kelas di mana satu adalah subkelas dari yang lain. Setelah semua subclass dijamin memiliki semua metode dan bidang yang dilakukan oleh kelas induknya, tetapi kebalikannya tidak benar. Oleh karena itu sementara subclass dapat lulus kapan saja Anda membutuhkan jenis kelas induk, jenis kelas induk harus ditolak jika Anda memerlukan jenis subclass.

Jika Anda menggabungkan ini dengan ketentuan bahwa semua nilai yang ditentukan pengguna harus merupakan instance dari sebuah kelas, maka Anda dapat is subclass ofmenarik dua tugas dan menyingkirkannya is subtype of.

Dan ini membawa kita pada karakteristik yang dimiliki oleh sebagian besar bahasa OO yang diketik secara statis. Ada satu set "primitif" jenis (misalnya int, float, dll) yang tidak terkait dengan setiap kelas dan tidak ditetapkan pengguna. Kemudian Anda memiliki semua kelas yang ditentukan pengguna yang secara otomatis memiliki jenis nama yang sama dan mengidentifikasi subkelas dengan subtyping.

Catatan terakhir yang akan saya buat adalah tentang kekakuan dari mendeklarasikan tipe secara terpisah dari nilai. Sebagian besar bahasa mengacaukan pembuatan keduanya, sehingga deklarasi tipe juga merupakan deklarasi untuk menghasilkan nilai yang sama sekali baru yang secara otomatis dilabeli dengan tipe itu. Sebagai contoh, deklarasi kelas biasanya menciptakan tipe dan juga cara instantiating nilai dari tipe itu. Ini menghilangkan beberapa kecerobohan dan, di hadapan konstruktor, juga memungkinkan Anda membuat label nilai tak terhingga banyak dengan tipe dalam satu pukulan.

badcook
sumber