Jaki jest dokładny problem z wielokrotnym dziedziczeniem?

121

Widzę ludzi, którzy cały czas pytają, czy dziedziczenie wielokrotne powinno zostać uwzględnione w następnej wersji C # lub Javy. Osoby korzystające z C ++, które mają tyle szczęścia, że ​​mają taką możliwość, mówią, że to tak, jakby dać komuś linę, aby w końcu się powiesić.

O co chodzi z wielokrotnym dziedziczeniem? Czy są jakieś konkretne próbki?

Vlad Gudim
źródło
54
Chciałbym tylko wspomnieć, że C ++ jest świetny do zapewnienia wystarczająco dużo liny, aby się powiesić.
tloach
1
Aby uzyskać alternatywę dla wielokrotnego dziedziczenia, która rozwiązuje (i rozwiązuje IMHO) wiele z tych samych problemów, spójrz na Traits ( iam.unibe.ch/~scg/Research/Traits )
Bevan
52
Myślałem, że C ++ daje ci wystarczająco dużo liny, by strzelić sobie w stopę.
KeithB,
6
To pytanie wydaje się zakładać, że istnieje problem z MI w ogóle, podczas gdy znalazłem wiele języków, w których MI jest używany na co dzień. Z pewnością istnieją problemy z obsługą MI w niektórych językach, ale nie jestem świadomy, że MI generalnie wiąże się z poważnymi problemami.
David Thornley,

Odpowiedzi:

86

Najbardziej oczywistym problemem jest nadpisywanie funkcji.

Powiedzmy, że mamy dwie klasy Ai Bobie definiują metodę doSomething. Teraz definiujesz trzecią klasę C, która dziedziczy po obu Ai B, ale nie nadpisujesz doSomethingmetody.

Kiedy kompilator wypełni ten kod ...

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

... której implementacji metody należy użyć? Bez dalszych wyjaśnień kompilator nie jest w stanie rozwiązać tej niejednoznaczności.

Oprócz zastępowania, innym dużym problemem związanym z dziedziczeniem wielokrotnym jest układ fizycznych obiektów w pamięci.

Języki takie jak C ++ i Java i C # tworzą stały układ oparty na adresach dla każdego typu obiektu. Coś takiego:

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

Kiedy kompilator generuje kod maszynowy (lub kod bajtowy), używa tych liczbowych przesunięć, aby uzyskać dostęp do każdej metody lub pola.

Wielokrotne dziedziczenie sprawia, że ​​jest to bardzo trudne.

Jeśli klasa Cdziedziczy po obu Ai B, kompilator musi zdecydować, czy ABuporządkować dane w kolejności, czy w BAkolejności.

Ale teraz wyobraź sobie, że wywołujesz metody na Bobiekcie. Czy to naprawdę tylko B? A może jest to faktycznie Cobiekt nazywany polimorficznie, poprzez swój Binterfejs? W zależności od faktycznej tożsamości obiektu fizyczny układ będzie inny i niemożliwe będzie poznanie przesunięcia funkcji do wywołania w miejscu wywołania.

Sposobem na obsługę tego rodzaju systemu jest porzucenie podejścia opartego na stałym układzie, pozwalając każdemu obiektowi na zapytanie o jego układ przed próbą wywołania funkcji lub uzyskania dostępu do jego pól.

A więc… w skrócie… obsługa wielokrotnego dziedziczenia jest utrapieniem dla autorów kompilatorów. Więc kiedy ktoś taki jak Guido van Rossum projektuje Pythona lub Anders Hejlsberg projektuje C #, wie, że obsługa dziedziczenia wielokrotnego znacznie skomplikuje implementacje kompilatora i prawdopodobnie nie uważa, że ​​korzyść jest warta poniesienia kosztów.

benjismith
źródło
62
Ehm, Python obsługuje MI
Nemanja Trifunovic
26
Nie są to zbyt przekonujące argumenty - kwestia stałego układu nie jest wcale trudna w większości języków; w C ++ jest to trudne, ponieważ pamięć nie jest nieprzezroczysta, a zatem możesz napotkać pewne trudności z założeniami arytmetycznymi wskaźnika. W językach, w których definicje klas są statyczne (jak w java, C # i C ++), wiele konfliktów nazw dziedziczenia może być zabronionych podczas kompilacji (a C # robi to tak czy inaczej z interfejsami!).
Eamon Nerbonne,
10
OP chciał tylko zrozumieć kwestie, a ja wyjaśniłem je bez osobiście redagowania tej sprawy. Właśnie powiedziałem, że projektanci języka i implementatorzy kompilatorów „prawdopodobnie nie sądzą, że korzyści są warte poniesionych kosztów”.
benjismith
12
Najbardziej oczywistym problemem jest przesłanianie funkcji. ” Nie ma to nic wspólnego z nadpisywaniem funkcji. To prosty problem z niejednoznacznością.
curiousguy
10
Ta odpowiedź zawiera błędne informacje o Guido i Pythonie, ponieważ Python obsługuje MI. „Zdecydowałem, że tak długo, jak będę wspierał dziedziczenie, równie dobrze mogę wspierać prostą wersję dziedziczenia wielokrotnego”. - Guido van Rossum python-history.blogspot.com/2009/02/… - Dodatkowo, rozwiązywanie niejednoznaczności jest dość powszechne w kompilatorach (zmienne mogą być lokalne dla bloku, lokalne dla funkcji, lokalne dla funkcji otaczającej, składowe obiektów, elementy składowe klas, globalne itp.), nie widzę, jak dodatkowy zakres miałby znaczenie.
marcus
46

Problemy, o których wspominacie, nie są naprawdę trudne do rozwiązania. W rzeczywistości np. Eiffel robi to doskonale! (i bez wprowadzania arbitralnych wyborów lub czegokolwiek)

Np. Jeśli dziedziczysz z A i B, oba mają metodę foo (), to oczywiście nie chcesz, aby arbitralny wybór w twojej klasie C dziedziczył zarówno z A, jak i B. Musisz albo przedefiniować foo, aby było jasne, co będzie używane, jeśli wywoływana jest c.foo () lub w innym przypadku musisz zmienić nazwę jednej z metod w C. (może się ona stać bar ())

Myślę też, że wielokrotne dziedziczenie jest często bardzo przydatne. Jeśli spojrzysz na biblioteki Eiffla, zobaczysz, że są one używane wszędzie i osobiście przegapiłem tę funkcję, kiedy musiałem wrócić do programowania w Javie.


źródło
26
Zgadzam się. Główny powód, dla którego ludzie nienawidzą MI, jest taki sam jak w przypadku JavaScript lub statycznego pisania: większość ludzi używała tylko bardzo złych implementacji tego mechanizmu - lub używała go bardzo źle. Ocenianie MI według C ++ jest jak ocenianie OOP przez PHP lub ocenianie samochodów przez Pintos.
Jörg W Mittag
2
@curiousguy: MI wprowadza kolejny zestaw komplikacji, o które należy się martwić, podobnie jak wiele „funkcji” C ++. Tylko dlatego, że jest jednoznaczny, nie ułatwia pracy ani debugowania. Usunięcie tego łańcucha, ponieważ wypadł z tematu i i tak go zdmuchnąłeś.
Guvante
4
@Guvante jedynym problemem związanym z MI w jakimkolwiek języku jest to, że gówni programiści myślą, że mogą przeczytać tutorial i nagle znają język.
Miles Rout
2
Twierdzę, że funkcje językowe to nie tylko skrócenie czasu kodowania. Chodzą także o zwiększenie wyrazistości języka i zwiększenie wydajności.
Miles Rout
4
Ponadto błędy pojawiają się w MI tylko wtedy, gdy idioci używają go nieprawidłowo.
Miles Rout
27

Problem z diamentami :

niejednoznaczność, która pojawia się, gdy dwie klasy B i C dziedziczą po A, a klasa D dziedziczy zarówno po B, jak i C.Jeśli istnieje metoda w A, którą B i C nadpisały , a D jej nie przesłania, to która wersja metody metoda D dziedziczy: tę z B, czy z C?

... Nazywa się to „problemem diamentów” ze względu na kształt diagramu dziedziczenia klas w tej sytuacji. W tym przypadku klasa A jest na górze, zarówno B, jak i C osobno pod nią, a D łączy oba razem na dole, tworząc kształt rombu ...

J Francis
źródło
4
który ma rozwiązanie znane jako dziedziczenie wirtualne. To tylko problem, jeśli zrobisz to źle.
Ian Goldby,
1
@IanGoldby: Dziedziczenie wirtualne jest mechanizmem do rozwiązania części problemu, jeśli nie trzeba zezwalać na upcasts i downcast z zachowaniem tożsamości wśród wszystkich typów, z których pochodzi instancja lub dla których można ją zastąpić . Biorąc pod uwagę X: B; Y: B; i Z: X, Y; załóżmy, że someZ jest instancją Z. W przypadku dziedziczenia wirtualnego (B) (X) someZ i (B) (Y) someZ są odrębnymi obiektami; biorąc pod uwagę jeden z nich, jeden może dostać drugiego przez przygnębienie i uniesienie, ale co, jeśli ktoś ma someZi chce go rzucić, Objecta potem do niego B? Który Bto dostanie?
supercat
2
@supercat Być może, ale takie problemy są w dużej mierze teoretyczne iw każdym przypadku mogą być sygnalizowane przez kompilator. Ważne jest, aby być świadomym problemu, który próbujesz rozwiązać, a następnie użyć najlepszego narzędzia, ignorując dogmat ludzi, którzy woleliby nie zajmować się zrozumieniem „dlaczego”?
Ian Goldby
@IanGoldby: Takie problemy mogą być sygnalizowane przez kompilator tylko wtedy, gdy ma on jednoczesny dostęp do wszystkich odpowiednich klas. W niektórych frameworkach każda zmiana w klasie bazowej zawsze będzie wymagać ponownej kompilacji wszystkich klas pochodnych, ale możliwość korzystania z nowszych wersji klas bazowych bez konieczności ponownej kompilacji klas pochodnych (dla których można nie mieć kodu źródłowego) jest użyteczną funkcją dla ram, które mogą to zapewnić. Co więcej, problemy nie są tylko teoretyczne. Wiele klas w .NET opiera się na fakcie, że rzutowanie z dowolnego typu referencyjnego do Objectiz powrotem do tego typu ...
Supercat,
3
@IanGoldby: W porządku. Chodziło mi o to, że osoby wdrażające Javę i .NET były nie tylko „leniwe”, decydując się nie wspierać uogólnionego MI; wspieranie uogólnionego MI uniemożliwiłoby ich modelowi podtrzymanie różnych aksjomatów, których ważność jest bardziej użyteczna dla wielu użytkowników niż MI.
supercat
21

Dziedziczenie wielokrotne to jedna z tych rzeczy, które nie są często używane i mogą być nadużywane, ale czasami są potrzebne.

Nigdy nie rozumiałem, jak nie dodawać funkcji, tylko dlatego, że może być niewłaściwie wykorzystana, gdy nie ma dobrych alternatyw. Interfejsy nie są alternatywą dla dziedziczenia wielokrotnego. Po pierwsze, nie pozwalają na egzekwowanie warunków wstępnych ani końcowych. Podobnie jak w przypadku każdego innego narzędzia, musisz wiedzieć, kiedy należy go użyć i jak z niego korzystać.

KeithB
źródło
Czy możesz wyjaśnić, dlaczego nie pozwalają na egzekwowanie warunków przed i po?
Yttrill,
2
@Yttrill, ponieważ interfejsy nie mogą mieć implementacji metod. Gdzie kładziesz assert?
ciekawy facet
1
@curiousguy: używasz języka z odpowiednią składnią, która pozwala na umieszczanie warunków wstępnych i końcowych bezpośrednio w interfejsie: nie jest potrzebne żadne „potwierdzenie”. Przykład z Felixa: fun div (num: int, den: int when den! = 0): int oczekiwać wynik == 0 implikuje num == 0;
Yttrill,
@Yttrill OK, ale niektóre języki, takie jak Java, nie obsługują ani MI ani „warunków wstępnych i końcowych bezpośrednio w interfejsie”.
curiousguy
Nie jest często używany, ponieważ nie jest dostępny i nie wiemy, jak go dobrze używać. Jeśli spojrzysz na kod Scala, zobaczysz, jak rzeczy zaczynają być powszechne i można je refaktoryzować na cechy (Ok, to nie jest MI, ale udowadnia mój punkt widzenia).
santiagobasulto
16

powiedzmy, że masz obiekty A i B, które są dziedziczone przez C. A i B zarówno implementują foo (), jak i C nie. Dzwonię do C.foo (). Która implementacja zostanie wybrana? Są inne problemy, ale tego typu są poważne.

tloach
źródło
1
Ale to nie jest konkretny przykład. Jeśli zarówno A, jak i B mają funkcję, jest bardzo prawdopodobne, że C będzie również potrzebować własnej implementacji. W przeciwnym razie nadal może wywołać A :: foo () w swojej własnej funkcji foo ().
Peter Kühne
@Quantum: A jeśli tak nie jest? Łatwo jest zobaczyć problem z jednym poziomem dziedziczenia, ale jeśli masz wiele poziomów i masz jakąś przypadkową funkcję, która jest gdzieś dwa razy, staje się to bardzo trudnym problemem.
tloach
Nie chodzi również o to, że nie możesz wywołać metody A lub B, określając, którą chcesz, chodzi o to, że jeśli nie określisz, nie ma dobrego sposobu, aby ją wybrać. Nie jestem pewien, jak C ++ sobie z tym radzi, ale jeśli ktoś wie, czy może o tym wspomnieć?
tloach
2
@tloach - jeśli C nie rozwiązuje niejednoznaczności, kompilator może wykryć ten błąd i zwrócić błąd w czasie kompilacji.
Eamon Nerbonne,
@Earmon - Ze względu na polimorfizm, jeśli foo () jest wirtualna, kompilator może nawet nie wiedzieć w czasie kompilacji, że będzie to problem.
tloach
5

Główny problem z wielokrotnym dziedziczeniem ładnie podsumowuje przykład Tloacha. Podczas dziedziczenia z wielu klas bazowych, które implementują tę samą funkcję lub pole, kompilator musi podjąć decyzję o tym, jaką implementację ma dziedziczyć.

Sytuacja pogarsza się, gdy dziedziczysz z wielu klas, które dziedziczą z tej samej klasy bazowej. (dziedziczenie diamentu, jeśli narysujesz drzewo dziedziczenia, otrzymasz kształt diamentu)

Te problemy nie są tak naprawdę problematyczne dla kompilatora. Ale wybór, jakiego musi dokonać kompilator, jest raczej arbitralny, co sprawia, że ​​kod jest znacznie mniej intuicyjny.

Uważam, że wykonując dobry projekt OO, nigdy nie potrzebuję wielokrotnego dziedziczenia. W przypadkach, gdy tego potrzebuję, zwykle okazuje się, że korzystam z dziedziczenia w celu ponownego wykorzystania funkcji, podczas gdy dziedziczenie jest odpowiednie tylko dla relacji „jest-a”.

Istnieją inne techniki, takie jak mieszanki, które rozwiązują te same problemy i nie mają problemów, które ma wielokrotne dziedziczenie.

Mendelt
źródło
4
Skompilowany nie musi dokonywać arbitralnego wyboru - może po prostu się pomylić. W C #, jaki jest typ ([..bool..]? "test": 1)?
Eamon Nerbonne
4
W C ++ kompilator nigdy nie dokonuje takich arbitralnych wyborów: błędem jest zdefiniowanie klasy, w której kompilator musiałby dokonać dowolnego wyboru.
curiousguy
5

Nie sądzę, że problem z diamentami jest problemem, rozważałbym tę sofistykę, nic więcej.

Z mojego punktu widzenia najgorszym problemem z wielokrotnym dziedziczeniem jest RAD - ofiary i ludzie, którzy twierdzą, że są programistami, ale w rzeczywistości mają połowę wiedzy (w najlepszym przypadku).

Osobiście byłbym bardzo szczęśliwy, gdybym mógł wreszcie zrobić coś takiego w Windows Forms (to nie jest poprawny kod, ale powinien dać ci pomysł):

public sealed class CustomerEditView : Form, MVCView<Customer>

Jest to główny problem związany z brakiem wielokrotnego dziedziczenia. MOŻESZ zrobić coś podobnego z interfejsami, ale jest coś, co nazywam „kodem s ***”, to jest to bolesne powtarzające się c ***, które musisz napisać w każdej ze swoich klas, na przykład, aby uzyskać kontekst danych.

Moim zdaniem nie powinno być absolutnie żadnego, ani najmniejszego, potrzeby powtarzania kodu we współczesnym języku.

Turing Complete
źródło
Zwykle się zgadzam, ale tylko: istnieje potrzeba wykrycia błędów w jakimkolwiek języku. W każdym razie powinieneś dołączyć do zespołu programistów Felix, ponieważ jest to główny cel. Na przykład wszystkie deklaracje są nawzajem rekurencyjne i możesz widzieć zarówno do przodu, jak i do tyłu, więc nie potrzebujesz deklaracji do przodu (zakres jest ustawiony, podobnie jak etykiety C goto).
Yttrill,
Całkowicie zgadzam się z tym - po prostu wpadł na podobny problem tutaj . Ludzie mówią o problemie diamentów, cytują go religijnie, ale moim zdaniem tak łatwo go uniknąć. (Nie wszyscy musimy pisać nasze programy tak, jak napisali bibliotekę iostream). Wielokrotne dziedziczenie powinno być logicznie używane, gdy masz obiekt, który potrzebuje funkcjonalności dwóch różnych klas bazowych, które nie mają nakładających się funkcji ani nazw funkcji. W odpowiednich rękach to narzędzie.
jedd.ahyoung
3
@Turing Complete: wrt bez powtórzeń kodu: to fajny pomysł, ale jest niepoprawny i niemożliwy. Istnieje ogromna liczba wzorców użycia i chcemy wyabstrahować powszechne wzorce w bibliotece, ale szaleństwem jest abstrahować je wszystkie, ponieważ nawet gdybyśmy mogli, obciążenie semantyczne zapamiętywania wszystkich nazw jest zbyt wysokie. To, czego chcesz, to dobra równowaga. Nie zapominaj, że to powtarzanie nadaje rzeczom strukturę (wzorzec oznacza nadmiarowość).
Yttrill
@ lunchmeat317: Fakt, że kod generalnie nie powinien być napisany w taki sposób, że „diament” stanowiłby problem, nie oznacza, że ​​projektant języka / frameworka może po prostu zignorować problem. Jeśli framework zapewnia, że ​​upcasting i downcasting zachowują tożsamość obiektu, chce zezwolić późniejszym wersjom klasy na zwiększenie liczby typów, dla których można ją zastąpić, bez zmiany przełomowej, i chce zezwolić na tworzenie typów w czasie wykonywania, Nie sądzę, że mogłoby to pozwolić na dziedziczenie wielu klas (w przeciwieństwie do dziedziczenia interfejsu) przy jednoczesnym spełnieniu powyższych celów.
supercat
3

Common Lisp Object System (CLOS) to kolejny przykład czegoś, co obsługuje MI, unikając jednocześnie problemów w stylu C ++: dziedziczenie ma sensowną wartość domyślną , a jednocześnie pozwala na jawne decydowanie, jak dokładnie, powiedzmy, wywołać zachowanie super .

Frank Shearar
źródło
Tak, CLOS to jeden z najlepszych systemów obiektowych od czasu powstania współczesnych komputerów, może nawet dawno temu :)
rostamn739
2

Nie ma nic złego w samym dziedziczeniu wielokrotnym. Problem polega na dodaniu wielokrotnego dziedziczenia do języka, który nie został zaprojektowany z myślą o dziedziczeniu wielokrotnym od samego początku.

Język Eiffla obsługuje dziedziczenie wielokrotne bez ograniczeń w bardzo wydajny i produktywny sposób, ale od tego momentu został zaprojektowany, aby go obsługiwać.

Ta funkcja jest skomplikowana do zaimplementowania dla programistów kompilatorów, ale wydaje się, że ta wada może zostać skompensowana przez fakt, że dobra obsługa wielokrotnego dziedziczenia mogłaby uniknąć obsługi innych funkcji (tj. Nie ma potrzeby stosowania interfejsu lub metody rozszerzenia).

Myślę, że wspieranie lub brak dziedziczenia wielokrotnego jest bardziej kwestią wyboru, kwestią priorytetów. Bardziej złożona funkcja wymaga więcej czasu, aby została poprawnie wdrożona i operacyjna i może być bardziej kontrowersyjna. Implementacja C ++ może być przyczyną, dla której dziedziczenie wielokrotne nie zostało zaimplementowane w C # i Javie ...

Christian Lemer
źródło
1
Obsługa C ++ dla MI nie jest „ bardzo wydajna i produktywna ”?
curiousguy
1
W rzeczywistości jest nieco zepsuty w tym sensie, że nie pasuje do innych funkcji C ++. Przypisanie nie działa poprawnie w przypadku dziedziczenia, nie mówiąc już o dziedziczeniu wielokrotnym (sprawdź naprawdę złe zasady). Prawidłowe tworzenie diamentów jest tak trudne, że komitet ds. Standardów schrzanił hierarchię wyjątków, aby była prosta i wydajna, zamiast robić to poprawnie. Na starszym kompilatorze, którego używałem w tamtym czasie, testowałem to, a kilka miksów MI i implementacji podstawowych wyjątków kosztowało ponad megabajt kodu, a kompilacja zajęła 10 minut… tylko definicje.
Yttrill,
1
Diamenty to dobry przykład. W Eiffelu diament jest wyraźnie rozpoznawany. Na przykład wyobraź sobie Ucznia i Nauczyciela dziedziczących po osobie. Osoba ma kalendarz, więc zarówno Uczeń, jak i Nauczyciel odziedziczą ten kalendarz. Jeśli zbudujesz diament, tworząc TeachingStudent, który dziedziczy zarówno po Nauczycielu, jak i Uczniu, możesz zdecydować o zmianie nazwy jednego z odziedziczonych kalendarzy, aby oba kalendarze były dostępne osobno, lub zdecydować się na ich połączenie, aby zachowywał się bardziej jak Osoba. Dziedziczenie wielokrotne można ładnie zaimplementować, ale wymaga to starannego zaprojektowania i najlepiej od samego początku ...
Christian Lemer
1
Kompilatorzy Eiffla muszą przeprowadzić globalną analizę programu, aby efektywnie wdrożyć ten model MI. W przypadku wywołań metod polimorficznych używają one albo thunks dyspozytora, albo rzadkich macierzy, jak wyjaśniono tutaj . Nie łączy się to dobrze z oddzielną kompilacją C ++ oraz funkcją ładowania klas C # i Javy.
cyco130
2

Jednym z celów projektowania frameworków, takich jak Java i .NET, jest umożliwienie skompilowanemu kodowi do pracy z jedną wersją wstępnie skompilowanej biblioteki, tak samo dobrze współpracuje z kolejnymi wersjami tej biblioteki, nawet jeśli te kolejne wersje dodać nowe funkcje. Podczas gdy normalnym paradygmatem w językach takich jak C lub C ++ jest dystrybucja połączonych statycznie plików wykonywalnych, które zawierają wszystkie potrzebne im biblioteki, paradygmatem w .NET i Javie jest dystrybucja aplikacji jako kolekcji komponentów, które są „połączone” w czasie wykonywania .

Model COM, który poprzedzał .NET, próbował zastosować to ogólne podejście, ale tak naprawdę nie miał dziedziczenia - zamiast tego każda definicja klasy skutecznie definiowała zarówno klasę, jak i interfejs o tej samej nazwie, który zawierał wszystkich publicznych członków. Instancje były typu klasowego, podczas gdy odwołania były typu interfejsu. Zadeklarowanie klasy jako pochodnej od innej było równoznaczne z zadeklarowaniem klasy jako implementującej interfejs drugiej osoby i wymagało, aby nowa klasa ponownie zaimplementowała wszystkie publiczne elementy członkowskie klas, z których się wywodzi. Jeśli Y i Z wywodzą się z X, a W wywodzi się z Y i Z, nie będzie miało znaczenia, czy Y i Z implementują elementy X inaczej, ponieważ Z nie będzie w stanie użyć ich implementacji - będzie musiał zdefiniować swoje posiadać. W może zawierać wystąpienia Y i / lub Z,

Trudność w Javie i .NET polega na tym, że kod może dziedziczyć elementy członkowskie i mieć do nich dostęp niejawnie odwołując się do elementów nadrzędnych. Załóżmy, że mamy klasy WZ powiązane jak wyżej:

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();
  }
}

Wydawałoby się, że W.Test()utworzenie instancji W powinno wywołać implementację metody wirtualnej Foozdefiniowanej w X. Załóżmy jednak, że Y i Z faktycznie znajdowały się w osobno skompilowanym module i chociaż zostały zdefiniowane jak powyżej, gdy kompilowano X i W, zostały później zmienione i ponownie skompilowane:

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

Jaki powinien być efekt wezwania W.Test()? Gdyby program musiał być statycznie połączony przed dystrybucją, etap łącza statycznego mógłby być w stanie stwierdzić, że chociaż program nie miał dwuznaczności przed zmianą Y i Z, zmiany w Y i Z sprawiły, że rzeczy stały się niejednoznaczne i konsolidator mógłby odmówić skompilować program, chyba że lub do czasu rozwiązania takiej niejednoznaczności. Z drugiej strony możliwe jest, że osoba, która ma zarówno W, jak i nowe wersje Y i Z, jest kimś, kto po prostu chce uruchomić program i nie ma kodu źródłowego żadnego z nich. Kiedy W.Test()biegnie, nie będzie już jasne, coW.Test() powinien zrobić, ale dopóki użytkownik nie spróbuje uruchomić W z nową wersją Y i Z, żadna część systemu nie będzie w stanie rozpoznać problemu (chyba że W został uznany za nielegalny nawet przed zmianami na Y i Z) .

superkat
źródło
2

Diament nie stanowi problemu, o ile nie używasz czegoś takiego jak dziedziczenie wirtualne w C ++: w normalnym dziedziczeniu każda klasa bazowa przypomina pole składowe (w rzeczywistości są one rozmieszczone w pamięci RAM w ten sposób), co daje trochę cukru syntaktycznego i dodatkowa możliwość zastępowania bardziej wirtualnych metod. Może to narzucać pewne niejasności w czasie kompilacji, ale zwykle jest to łatwe do rozwiązania.

Z drugiej strony, w przypadku dziedziczenia wirtualnego zbyt łatwo wymyka się spod kontroli (a następnie staje się bałaganem). Rozważmy jako przykład diagram „serca”:

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

W C ++ jest to całkowicie niemożliwe: jak najszybciej Fi Gsą połączone w jednej klasie, ich As są scalane też okres. Oznacza to, że nie może brać pod uwagę klasy bazowe nieprzezroczysty w C ++ (w tym przykładzie trzeba zbudować Aw Htak trzeba wiedzieć, że obecny gdzieś w hierarchii). Jednak w innych językach może działać; na przykład, Fi Gmógłby wyraźnie zadeklarować A jako „wewnętrzny”, zabraniając w ten sposób konsekwentnego łączenia i efektywnego budowania siebie.

Kolejny interesujący przykład ( nie specyficzny dla C ++):

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

Tutaj Bużywa tylko dziedziczenia wirtualnego. Więc Ezawiera dwa, Bktóre mają to samo A. W ten sposób możesz uzyskać A*wskaźnik, który wskazuje na E, ale nie możesz rzucić go na B*wskaźnik, chociaż obiekt jest w rzeczywistości B rzutowaniem niejednoznacznym, a tej niejednoznaczności nie można wykryć w czasie kompilacji (chyba że kompilator widzi cały program). Oto kod testowy:

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;
}

Ponadto implementacja może być bardzo złożona (zależy od języka; patrz odpowiedź Benjismitha).

liczba Zero
źródło
To jest prawdziwy problem z MI. Programiści mogą potrzebować różnych rozdzielczości w ramach jednej klasy. Rozwiązanie obejmujące cały język ograniczyłoby to, co jest możliwe i zmusiłoby programistów do tworzenia kludges, aby program działał poprawnie.
shawnhcorey