Apa masalah sebenarnya dengan multiple inheritance?

121

Saya dapat melihat orang-orang bertanya sepanjang waktu apakah beberapa warisan harus dimasukkan ke dalam versi C # atau Java berikutnya. Orang C ++, yang cukup beruntung memiliki kemampuan ini, mengatakan bahwa ini seperti memberi seseorang tali untuk akhirnya menggantung diri.

Ada apa dengan warisan berganda? Apakah ada contoh beton?

Vlad Gudim
sumber
54
Saya hanya akan menyebutkan bahwa C ++ bagus untuk memberi Anda cukup tali untuk menggantung diri Anda sendiri.
tloach
1
Untuk alternatif untuk beberapa pewarisan yang membahas (dan, IMHO memecahkan) banyak masalah yang sama, lihat Sifat ( iam.unibe.ch/~scg/Research/Traits )
Bevan
52
Saya pikir C ++ memberi Anda cukup tali untuk menembak diri Anda sendiri.
KeithB
6
Pertanyaan ini sepertinya berasumsi bahwa ada masalah dengan MI secara umum, padahal saya telah menemukan banyak bahasa di mana MI digunakan secara kasual. Tentu ada masalah dengan penanganan MI bahasa tertentu, tetapi saya tidak menyadari bahwa MI secara umum memiliki masalah yang signifikan.
David Thornley

Jawaban:

86

Masalah paling jelas adalah dengan fungsi overriding.

Katakanlah memiliki dua kelas Adan Bkeduanya mendefinisikan metode doSomething. Sekarang Anda mendefinisikan kelas ketiga C, yang mewarisi dari Adan B, tetapi Anda tidak mengganti doSomethingmetode.

Ketika kompilator memasukkan kode ini ...

C c = new C();
c.doSomething();

... implementasi metode mana yang harus digunakan? Tanpa klarifikasi lebih lanjut, compiler tidak mungkin dapat menyelesaikan ambiguitas tersebut.

Selain menimpa, masalah besar lainnya dengan multiple inheritance adalah tata letak objek fisik dalam memori.

Bahasa seperti C ++ dan Java dan C # membuat tata letak berbasis alamat tetap untuk setiap jenis objek. Sesuatu seperti ini:

class A:
    at offset 0 ... "abc" ... 4 byte int field
    at offset 4 ... "xyz" ... 8 byte double field
    at offset 12 ... "speak" ... 4 byte function pointer

class B:
    at offset 0 ... "foo" ... 2 byte short field
    at offset 2 ... 2 bytes of alignment padding
    at offset 4 ... "bar" ... 4 byte array pointer
    at offset 8 ... "baz" ... 4 byte function pointer

Saat compiler menghasilkan kode mesin (atau bytecode), ia menggunakan offset numerik tersebut untuk mengakses setiap metode atau kolom.

Warisan ganda membuatnya sangat rumit.

Jika kelas Cmewarisi dari Adan B, kompilator harus memutuskan apakah akan menyusun data secara ABberurutan atau BAberurutan.

Tapi sekarang bayangkan Anda memanggil metode pada suatu Bobjek. Apakah ini benar-benar hanya a B? Atau apakah itu sebenarnya sebuah Cobjek yang disebut secara polimorfis, melalui Bantarmukanya? Bergantung pada identitas objek yang sebenarnya, tata letak fisik akan berbeda, dan tidak mungkin mengetahui offset fungsi yang akan dipanggil di situs panggilan.

Cara menangani sistem semacam ini adalah dengan membuang pendekatan tata letak tetap, yang memungkinkan setiap objek ditanyai tata letaknya sebelum mencoba memanggil fungsi atau mengakses bidangnya.

Jadi ... singkat cerita ... sulit bagi penulis kompiler untuk mendukung banyak warisan. Jadi ketika seseorang seperti Guido van Rossum mendesain python, atau ketika Anders Hejlsberg mendesain c #, mereka tahu bahwa mendukung multiple inheritance akan membuat implementasi compiler jauh lebih kompleks, dan mungkin mereka tidak berpikir bahwa keuntungannya sepadan dengan biayanya.

benjismith
sumber
62
Ehm, Python mendukung MI
Nemanja Trifunovic
26
Argumen ini tidak terlalu meyakinkan - tata letak tetap sama sekali tidak rumit di sebagian besar bahasa; di C ++ ini rumit karena memori tidak buram dan dengan demikian Anda mungkin mengalami beberapa kesulitan dengan asumsi aritmatika penunjuk. Dalam bahasa di mana definisi kelasnya statis (seperti di java, C # dan C ++), beberapa nama pewarisan bentrok dapat dilarang waktu kompilasi (dan C # melakukan ini bagaimanapun juga dengan antarmuka!).
Eamon Nerbonne
10
OP hanya ingin memahami masalahnya, dan saya menjelaskannya tanpa editorial secara pribadi tentang masalah tersebut. Saya baru saja mengatakan bahwa perancang bahasa dan pelaksana kompilator "mungkin tidak berpikir manfaatnya sepadan dengan biayanya".
benjismith
12
" Masalah yang paling jelas adalah dengan penggantian fungsi. " Ini tidak ada hubungannya dengan penggantian fungsi. Ini masalah ambiguitas sederhana.
penasaran
10
Jawaban ini memiliki beberapa info yang salah tentang Guido dan Python, karena Python mendukung MI. "Saya memutuskan bahwa selama saya akan mendukung warisan, saya mungkin juga mendukung versi sederhana dari beberapa warisan." - Guido van Rossum python-history.blogspot.com/2009/02/… - Ditambah, resolusi ambiguitas cukup umum di compiler (variabel bisa lokal untuk memblokir, lokal ke fungsi, lokal ke fungsi penutup, anggota objek, anggota kelas, global, dll.), Saya tidak melihat bagaimana cakupan tambahan akan membuat perbedaan.
marcus
46

Masalah yang kalian sebutkan sebenarnya tidak terlalu sulit untuk dipecahkan. Faktanya, misalnya Eiffel melakukannya dengan sangat baik! (dan tanpa memberikan pilihan sewenang-wenang atau apa pun)

Misalnya jika Anda mewarisi dari A dan B, keduanya memiliki metode foo (), maka tentu saja Anda tidak ingin pilihan sewenang-wenang di kelas C Anda yang mewarisi dari A dan B. Anda harus mendefinisikan ulang foo sehingga jelas apa yang akan terjadi. digunakan jika c.foo () dipanggil atau jika tidak, Anda harus mengganti nama salah satu metode di C. (bisa menjadi bar ())

Saya juga berpikir bahwa warisan berganda seringkali cukup berguna. Jika Anda melihat perpustakaan Eiffel, Anda akan melihat bahwa itu digunakan di semua tempat dan secara pribadi saya melewatkan fitur itu ketika saya harus kembali ke pemrograman di Java.


sumber
26
Saya setuju. Alasan utama mengapa orang membenci MI adalah sama dengan JavaScript atau dengan pengetikan statis: kebanyakan orang hanya pernah menggunakan implementasi yang sangat buruk darinya - atau telah menggunakannya dengan sangat buruk. Menilai MI oleh C ++ seperti menilai OOP dengan PHP atau menilai mobil menurut Pintos.
Jörg W Mittag
2
@curiousguy: MI memperkenalkan satu set komplikasi lain yang perlu dikhawatirkan, seperti kebanyakan "fitur" C ++. Hanya karena tidak ambigu tidak membuatnya mudah untuk bekerja dengan atau debug. Menghapus rantai ini karena keluar dari topik dan Anda tetap gagal.
Guvante
4
@Guvante, satu-satunya masalah dengan MI dalam bahasa apa pun adalah programmer yang menyebalkan mengira mereka dapat membaca tutorial dan tiba-tiba tahu sebuah bahasa.
Miles Rout
2
Saya berpendapat bahwa fitur bahasa tidak hanya tentang mengurangi waktu pengkodean. Mereka juga tentang meningkatkan ekspresi bahasa, dan meningkatkan performa.
Miles Rout
4
Selain itu, bug hanya terjadi dari MI ketika para idiot menggunakannya secara tidak benar.
Miles Rout
27

Masalah berlian :

ambiguitas yang muncul ketika dua kelas B dan C mewarisi dari A, dan kelas D mewarisi dari B dan C.Jika ada metode di A yang telah diganti oleh B dan C , dan D tidak menimpanya, versi mana dari metode D mewarisi: dari B, atau dari C?

... Ini disebut "masalah berlian" karena bentuk diagram pewarisan kelas dalam situasi ini. Dalam hal ini, kelas A berada di atas, B dan C secara terpisah di bawahnya, dan D menggabungkan keduanya bersama-sama di bawah untuk membentuk bentuk berlian ...

J Francis
sumber
4
yang memiliki solusi yang dikenal sebagai warisan virtual. Ini hanya masalah jika Anda melakukannya dengan salah.
Ian Goldby
1
@IanGoldby: Warisan virtual adalah mekanisme untuk menyelesaikan bagian dari masalah, jika seseorang tidak perlu mengizinkan orang-orang tersisih dan tertekan yang melestarikan identitas di antara semua jenis asal instance atau yang dapat diganti . Diberikan X: B; Y: B; dan Z: X, Y; asumsikan someZ adalah turunan dari Z. Dengan warisan virtual, (B) (X) someZ dan (B) (Y) someZ adalah objek yang berbeda; diberikan baik, seseorang bisa mendapatkan yang lain melalui putus asa dan putus asa, tetapi bagaimana jika seseorang memiliki someZdan ingin melemparkannya ke Objectdan kemudian ke B? Yang mana yang Bakan didapatnya?
supercat
2
@supercat Mungkin, tetapi masalah seperti itu sebagian besar bersifat teoritis, dan dalam hal apa pun dapat ditandai oleh kompilator. Hal yang penting adalah menyadari masalah apa yang Anda coba selesaikan, dan kemudian menggunakan alat terbaik, mengabaikan dogma dari orang-orang yang lebih suka tidak menyibukkan diri dengan pemahaman 'mengapa?'
Ian Goldby
@IanGoldby: Masalah seperti itu hanya dapat ditandai oleh kompilator jika ia memiliki akses simultan ke semua kelas yang dimaksud. Dalam beberapa kerangka kerja, setiap perubahan pada kelas dasar akan selalu memerlukan kompilasi ulang dari semua kelas turunan, tetapi kemampuan untuk menggunakan versi kelas dasar yang lebih baru tanpa harus mengkompilasi ulang kelas turunan (yang mungkin tidak memiliki kode sumber) adalah fitur yang berguna untuk kerangka kerja yang dapat menyediakannya. Lebih jauh, masalahnya bukan hanya teoritis. Banyak kelas di .NET bergantung pada fakta bahwa pemeran dari jenis referensi apa pun ke Objectdan kembali ke jenis itu ...
supercat
3
@IanGoldby: Cukup adil. Maksud saya adalah bahwa pelaksana Java dan .NET tidak hanya "malas" dalam memutuskan untuk tidak mendukung MI umum; mendukung MI umum akan mencegah kerangka mereka dari menegakkan berbagai aksioma yang validitasnya lebih berguna bagi banyak pengguna daripada MI.
supercat
21

Warisan berganda adalah salah satu dari hal-hal yang tidak sering digunakan, dan dapat disalahgunakan, tetapi terkadang diperlukan.

Saya tidak pernah mengerti tidak menambahkan fitur, hanya karena itu mungkin disalahgunakan, ketika tidak ada alternatif yang baik. Antarmuka bukanlah alternatif untuk multiple inheritance. Pertama, mereka tidak mengizinkan Anda menerapkan prasyarat atau prasyarat. Sama seperti alat lainnya, Anda perlu tahu kapan alat itu cocok untuk digunakan, dan bagaimana cara menggunakannya.

KeithB
sumber
Bisakah Anda menjelaskan mengapa mereka tidak mengizinkan Anda menerapkan ketentuan sebelum dan sesudah?
Yttrill
2
@Yttrill karena antarmuka tidak dapat memiliki implementasi metode. Di mana Anda meletakkan assert?
penasaran
1
@curiousguy: Anda menggunakan bahasa dengan sintaks yang sesuai yang memungkinkan Anda untuk menempatkan kondisi sebelum dan sesudah langsung ke antarmuka: tidak perlu "menegaskan". Contoh dari Felix: fun div (num: int, den: int when den! = 0): int mengharapkan hasil == 0 menyiratkan num == 0;
Yttrill
@Yttrill Oke, tetapi beberapa bahasa, seperti Java, tidak mendukung MI atau "kondisi sebelum dan sesudah langsung ke antarmuka".
penasaran
Ini tidak sering digunakan karena tidak tersedia, dan kami tidak tahu bagaimana menggunakannya dengan baik. Jika Anda melihat beberapa kode Scala, Anda akan melihat bagaimana hal-hal mulai menjadi umum dan dapat direfraktorisasi menjadi ciri-ciri (Oke, ini bukan MI, tapi buktikan maksud saya).
santiagobasulto
16

katakanlah Anda memiliki objek A dan B yang keduanya diwarisi oleh C. A dan B keduanya mengimplementasikan foo () dan C tidak. Saya sebut C.foo (). Implementasi mana yang dipilih? Ada masalah lain, tetapi hal seperti ini adalah masalah besar.

tloach
sumber
1
Tapi itu bukan contoh konkret. Jika A dan B memiliki fungsi, kemungkinan besar C juga membutuhkan implementasinya sendiri. Jika tidak, ia masih bisa memanggil A :: foo () dalam fungsi foo () itu sendiri.
Peter Kühne
@ Quantum: Bagaimana jika tidak? Sangat mudah untuk melihat masalah dengan satu tingkat pewarisan, tetapi jika Anda memiliki banyak tingkat dan Anda memiliki beberapa fungsi acak di suatu tempat dua kali, ini menjadi masalah yang sangat sulit.
tloach
Juga, intinya bukanlah bahwa Anda tidak dapat memanggil metode A atau B dengan menentukan mana yang Anda inginkan, intinya adalah jika Anda tidak menentukan maka tidak ada cara yang baik untuk memilihnya. Saya tidak yakin bagaimana C ++ menangani ini, tetapi jika seseorang tahu dapatkah menyebutkannya?
tloach
2
@tloach - jika C tidak menyelesaikan ambiguitas, kompilator dapat mendeteksi kesalahan ini dan mengembalikan kesalahan waktu kompilasi.
Eamon Nerbonne
@Earmon - Karena polimorfisme, jika foo () adalah virtual, kompilator bahkan mungkin tidak tahu pada saat kompilasi bahwa ini akan menjadi masalah.
tloach
5

Masalah utama dengan beberapa warisan diringkas dengan baik dengan contoh tloach. Saat mewarisi dari beberapa kelas dasar yang mengimplementasikan fungsi atau bidang yang sama, kompilator harus membuat keputusan tentang implementasi apa yang akan diwarisi.

Ini menjadi lebih buruk ketika Anda mewarisi dari beberapa kelas yang mewarisi dari kelas dasar yang sama. (warisan berlian, jika Anda menggambar pohon warisan, Anda mendapatkan bentuk berlian)

Masalah-masalah ini tidak terlalu bermasalah untuk diatasi oleh kompiler. Tetapi pilihan yang harus dibuat oleh compiler di sini agak sewenang-wenang, ini membuat kode menjadi kurang intuitif.

Saya menemukan bahwa ketika melakukan desain OO yang baik, saya tidak pernah membutuhkan multiple inheritance. Dalam kasus saya benar-benar membutuhkannya, saya biasanya menemukan saya telah menggunakan warisan untuk menggunakan kembali fungsionalitas sementara warisan hanya sesuai untuk hubungan "is-a".

Ada teknik lain seperti mixin yang memecahkan masalah yang sama dan tidak memiliki masalah yang dimiliki beberapa pewarisan.

Mendelt
sumber
4
Kompilasi tidak perlu membuat pilihan yang sewenang-wenang - itu bisa saja salah. Di C #, apa jenisnya ([..bool..]? "test": 1)?
Eamon Nerbonne
4
Dalam C ++, kompilator tidak pernah membuat pilihan sewenang-wenang seperti itu: merupakan kesalahan untuk menentukan kelas di mana kompilator perlu membuat pilihan sewenang-wenang.
penasaran
5

Saya tidak berpikir masalah berlian adalah masalah, saya akan menganggap itu menyesatkan, tidak ada yang lain.

Masalah terburuk, dari sudut pandang saya, dengan multiple inheritance adalah RAD - korban dan orang-orang yang mengaku sebagai pengembang tetapi pada kenyataannya terjebak dengan setengah - pengetahuan (paling banter).

Secara pribadi, saya akan sangat senang jika saya akhirnya dapat melakukan sesuatu di Windows Forms seperti ini (ini bukan kode yang benar, tetapi itu akan memberi Anda ide):

public sealed class CustomerEditView : Form, MVCView<Customer>

Ini adalah masalah utama yang saya alami karena tidak memiliki banyak warisan. Anda BISA melakukan sesuatu yang mirip dengan antarmuka, tetapi ada yang saya sebut "kode ***", ini adalah c *** berulang yang menyakitkan yang harus Anda tulis di setiap kelas untuk mendapatkan konteks data, misalnya.

Menurut pendapat saya, sama sekali tidak perlu, tidak sedikit pun, untuk pengulangan kode APAPUN dalam bahasa modern.

Turing Lengkap
sumber
Saya cenderung setuju, tetapi hanya cenderung: ada kebutuhan dari beberapa redundansi dalam bahasa apa pun untuk mendeteksi kesalahan. Bagaimanapun Anda harus bergabung dengan tim pengembang Felix karena itu adalah tujuan inti. Misalnya, semua deklarasi saling rekursif, dan Anda dapat melihat ke depan dan ke belakang sehingga Anda tidak memerlukan deklarasi ke depan (cakupan diatur, seperti label C goto).
Yttrill
Saya sepenuhnya setuju dengan ini - Saya baru saja mengalami masalah serupa di sini . Orang-orang berbicara tentang masalah intan, mereka mengutipnya secara religius, tetapi menurut saya hal itu sangat mudah dihindari. (Kita tidak semua perlu menulis program kita seperti yang mereka tulis di pustaka iostream.) Beberapa pewarisan secara logis harus digunakan ketika Anda memiliki objek yang membutuhkan fungsionalitas dari dua kelas dasar berbeda yang tidak memiliki fungsi atau nama fungsi yang tumpang tindih. Di tangan kanan, ini adalah alat.
jedd.ahyoung
3
@Turing Complete: wrt tidak memiliki pengulangan kode: ini adalah ide yang bagus tetapi tidak benar dan tidak mungkin. Ada sejumlah besar pola penggunaan dan kami ingin mengabstraksi yang umum ke dalam perpustakaan, tetapi kegilaan untuk mengabstraksi semuanya karena meskipun kami dapat memuat semantik mengingat semua nama terlalu tinggi. Yang Anda inginkan adalah keseimbangan yang bagus. Jangan lupa pengulangan adalah apa yang memberi struktur (pola menyiratkan redundansi).
Yttrill
@ lunchmeat317: Fakta bahwa kode umumnya tidak boleh ditulis sedemikian rupa sehingga 'berlian' akan menimbulkan masalah, tidak berarti bahwa perancang bahasa / kerangka kerja dapat mengabaikan masalah ini begitu saja. Jika kerangka kerja menyediakan upcasting dan downcasting mempertahankan identitas objek, ingin mengizinkan versi kelas yang lebih baru untuk meningkatkan jumlah jenis yang dapat diganti tanpa perubahan yang mengganggu, dan ingin mengizinkan pembuatan jenis run-time, Saya tidak berpikir itu bisa memungkinkan beberapa warisan kelas (sebagai lawan dari warisan antarmuka) sambil memenuhi tujuan di atas.
supercat
3

Common Lisp Object System (CLOS) adalah contoh lain dari sesuatu yang mendukung MI sambil menghindari masalah gaya C ++: pewarisan diberikan default yang masuk akal , sambil tetap memungkinkan Anda kebebasan untuk secara eksplisit memutuskan bagaimana tepatnya, katakanlah, memanggil perilaku super .

Frank Shearar
sumber
Ya, CLOS adalah salah satu sistem objek paling unggul sejak dimulainya komputasi modern bahkan mungkin sudah lama sekali :)
rostamn739
2

Tidak ada yang salah dalam multiple inheritance itu sendiri. Masalahnya adalah menambahkan beberapa pewarisan ke bahasa yang tidak dirancang dengan banyak pewarisan sejak awal.

Bahasa Eiffel mendukung banyak pewarisan tanpa batasan dengan cara yang sangat efisien dan produktif, tetapi bahasa itu dirancang sejak awal untuk mendukungnya.

Fitur ini kompleks untuk diterapkan pada pengembang kompilator, tetapi tampaknya kelemahan itu dapat dikompensasi oleh fakta bahwa dukungan multiple inheritance yang baik dapat menghindari dukungan fitur lain (yaitu tidak perlu Antarmuka atau Metode Ekstensi).

Saya pikir mendukung multiple inheritance atau tidak lebih merupakan masalah pilihan, masalah prioritas. Fitur yang lebih kompleks membutuhkan lebih banyak waktu untuk diterapkan dan beroperasi dengan benar dan mungkin lebih kontroversial. Implementasi C ++ mungkin menjadi alasan mengapa multiple inheritance tidak diimplementasikan di C # dan Java ...

Christian Lemer
sumber
1
Dukungan C ++ untuk MI tidak " sangat efisien dan produktif "?
penasaran
1
Sebenarnya ini agak rusak dalam arti tidak cocok dengan fitur C ++ lainnya. Penugasan tidak berfungsi dengan baik dengan warisan, apalagi beberapa warisan (lihat aturan yang benar-benar buruk). Membuat berlian dengan benar sangat sulit sehingga komite Standar mengacaukan hierarki pengecualian agar tetap sederhana dan efisien, daripada melakukannya dengan benar. Pada kompiler lama yang saya gunakan pada saat saya menguji ini dan beberapa MI mixin dan implementasi pengecualian dasar menghabiskan biaya lebih dari satu Megabyte kode dan membutuhkan 10 menit untuk mengkompilasi .. hanya definisi.
Yttrill
1
Berlian adalah contoh yang bagus. Di Eiffel, berlian diselesaikan secara eksplisit. Misalnya, bayangkan Siswa dan Guru mewarisi dari Orang. Orang tersebut memiliki kalender, sehingga Siswa dan Guru akan mewarisi kalender ini. Jika Anda membangun berlian dengan membuat TeachingStudent yang mewarisi dari Guru dan Murid, Anda dapat memutuskan untuk mengganti nama salah satu kalender yang diwariskan agar kedua kalender tetap tersedia secara terpisah atau memutuskan untuk menggabungkannya sehingga berperilaku lebih seperti Orang. Warisan ganda dapat diterapkan dengan baik, tetapi membutuhkan desain yang cermat dan sebaiknya sejak awal ...
Christian Lemer
1
Penyusun Eiffel harus melakukan analisis program global untuk mengimplementasikan model MI ini secara efisien. Untuk panggilan metode polimorfik mereka menggunakan baik dispatcher thunks atau matriks renggang seperti yang dijelaskan di sini . Ini tidak cocok dengan kompilasi terpisah C ++ dan fitur pemuatan kelas C # dan Java.
cyco130
2

Salah satu tujuan desain kerangka kerja seperti Java dan .NET adalah memungkinkan kode yang dikompilasi untuk bekerja dengan satu versi pustaka yang telah dikompilasi sebelumnya, untuk bekerja sama baiknya dengan versi pustaka berikutnya, bahkan jika versi berikutnya tambahkan fitur baru. Sementara paradigma normal dalam bahasa seperti C atau C ++ adalah mendistribusikan executable yang ditautkan secara statis yang berisi semua pustaka yang mereka butuhkan, paradigma dalam .NET dan Java adalah mendistribusikan aplikasi sebagai kumpulan komponen yang "ditautkan" pada waktu proses .

Model COM yang mendahului .NET mencoba menggunakan pendekatan umum ini, tetapi tidak benar-benar memiliki warisan - sebagai gantinya, setiap definisi kelas secara efektif mendefinisikan kelas dan antarmuka dengan nama yang sama yang berisi semua anggota publiknya. Contoh adalah tipe kelas, sedangkan referensi adalah tipe antarmuka. Mendeklarasikan kelas sebagai turunan dari kelas lain sama dengan mendeklarasikan kelas sebagai mengimplementasikan antarmuka lain, dan mengharuskan kelas baru untuk mengimplementasikan ulang semua anggota publik dari kelas yang diturunkan. Jika Y dan Z diturunkan dari X, lalu W berasal dari Y dan Z, tidak masalah jika Y dan Z mengimplementasikan anggota X secara berbeda, karena Z tidak akan dapat menggunakan implementasinya - itu harus menentukan sendiri. W mungkin merangkum instance Y dan / atau Z,

Kesulitan di Java dan .NET adalah bahwa kode diizinkan untuk mewarisi anggota dan memiliki akses ke mereka secara implisit merujuk ke anggota induk. Misalkan seseorang memiliki kelas WZ terkait seperti di atas:

class X { public virtual void Foo() { Console.WriteLine("XFoo"); }
class Y : X {};
class Z : X {};
class W : Y, Z  // Not actually permitted in C#
{
  public static void Test()
  {
    var it = new W();
    it.Foo();
  }
}

Sepertinya W.Test()harus membuat instance panggilan W implementasi metode virtual yang Fooditentukan dalam X. Misalkan, bagaimanapun, bahwa Y dan Z sebenarnya berada dalam modul yang dikompilasi secara terpisah, dan meskipun mereka didefinisikan seperti di atas ketika X dan W dikompilasi, mereka kemudian diubah dan dikompilasi ulang:

class Y : X { public override void Foo() { Console.WriteLine("YFoo"); }
class Z : X { public override void Foo() { Console.WriteLine("ZFoo"); }

Sekarang apa yang seharusnya menjadi efek dari panggilan W.Test()? Jika program harus ditautkan secara statis sebelum didistribusikan, tahap tautan statis mungkin dapat membedakan bahwa meskipun program tidak memiliki ambiguitas sebelum Y dan Z diubah, perubahan ke Y dan Z telah membuat hal-hal menjadi ambigu dan penaut dapat menolak untuk membangun program kecuali atau sampai ambiguitas tersebut diselesaikan. Di sisi lain, mungkin saja orang yang memiliki W dan versi baru Y dan Z adalah seseorang yang hanya ingin menjalankan program dan tidak memiliki kode sumber untuk semua itu. Saat W.Test()berlari, tidak akan jelas lagi apaW.Test() harus dilakukan, tetapi hingga pengguna mencoba menjalankan W dengan versi baru Y dan Z, tidak mungkin ada bagian dari sistem yang dapat mengenali adanya masalah (kecuali W dianggap tidak sah bahkan sebelum perubahan ke Y dan Z) .

supercat
sumber
2

Berlian tidak menjadi masalah, selama Anda tidak menggunakan apa pun seperti warisan virtual C ++: dalam warisan normal setiap kelas dasar menyerupai bidang anggota (sebenarnya mereka ditata dalam RAM dengan cara ini), memberi Anda gula sintaksis dan kemampuan ekstra untuk mengganti metode virtual lainnya. Itu mungkin menimbulkan beberapa ambiguitas pada waktu kompilasi tetapi biasanya mudah dipecahkan.

Di sisi lain, dengan warisan virtual itu terlalu mudah lepas kendali (dan kemudian menjadi berantakan). Pertimbangkan sebagai contoh diagram "hati":

  A       A
 / \     / \
B   C   D   E
 \ /     \ /
  F       G
    \   /
      H

Di C ++ sama sekali tidak mungkin: segera setelah Fdan Gdigabungkan menjadi satu kelas, As mereka juga digabungkan, titik. Itu berarti Anda mungkin tidak pernah mempertimbangkan kelas dasar Buram di C ++ (dalam contoh ini Anda harus membangun Adi Hsehingga Anda harus tahu bahwa itu suatu tempat dalam hirarki). Namun, dalam bahasa lain ini mungkin berhasil; misalnya, Fdan Gsecara eksplisit dapat mendeklarasikan A sebagai "internal", sehingga melarang penggabungan konsekuen dan secara efektif membuat diri mereka solid.

Contoh menarik lainnya ( bukan khusus C ++):

  A
 / \
B   B
|   |
C   D
 \ /
  E

Di sini, hanya Bmenggunakan warisan virtual. Jadi Eberisi dua Byang berbagi sama A. Dengan cara ini, Anda bisa mendapatkan A*pointer yang menunjuk ke E, tetapi Anda tidak bisa dilemparkan ke sebuah B*pointer meskipun objek ini sebenarnya B sebagai pemeran tersebut ambigu, dan ambiguitas ini tidak dapat terdeteksi pada waktu kompilasi (kecuali compiler melihat seluruh program). Ini kode tesnya:

struct A { virtual ~A() {} /* so that the class is polymorphic */ };
struct B: virtual A {};
struct C: B {};
struct D: B {};
struct E: C, D {};

int main() {
        E data;
        E *e = &data;
        A *a = dynamic_cast<A *>(e); // works, A is unambiguous
//      B *b = dynamic_cast<B *>(e); // doesn't compile
        B *b = dynamic_cast<B *>(a); // NULL: B is ambiguous
        std::cout << "E: " << e << std::endl;
        std::cout << "A: " << a << std::endl;
        std::cout << "B: " << b << std::endl;
// the next casts work
        std::cout << "A::C::B: " << dynamic_cast<B *>(dynamic_cast<C *>(e)) << std::endl;
        std::cout << "A::D::B: " << dynamic_cast<B *>(dynamic_cast<D *>(e)) << std::endl;
        std::cout << "A=>C=>B: " << dynamic_cast<B *>(dynamic_cast<C *>(a)) << std::endl;
        std::cout << "A=>D=>B: " << dynamic_cast<B *>(dynamic_cast<D *>(a)) << std::endl;
        return 0;
}

Selain itu, implementasinya mungkin sangat kompleks (bergantung pada bahasa; lihat jawaban benjismith).

nomor Nol
sumber
Itulah masalah sebenarnya dengan MI. Pemrogram mungkin memerlukan resolusi berbeda dalam satu kelas. Solusi berskala bahasa akan membatasi apa yang mungkin dan memaksa pemrogram untuk membuat kludges agar program bekerja dengan benar.
shawnhcorey