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?
oop
multiple-inheritance
language-theory
Vlad Gudim
źródło
źródło
Odpowiedzi:
Najbardziej oczywistym problemem jest nadpisywanie funkcji.
Powiedzmy, że mamy dwie klasy
A
iB
obie definiują metodędoSomething
. Teraz definiujesz trzecią klasęC
, która dziedziczy po obuA
iB
, ale nie nadpisujeszdoSomething
metody.Kiedy kompilator wypełni ten kod ...
... 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:
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
C
dziedziczy po obuA
iB
, kompilator musi zdecydować, czyAB
uporządkować dane w kolejności, czy wBA
kolejności.Ale teraz wyobraź sobie, że wywołujesz metody na
B
obiekcie. Czy to naprawdę tylkoB
? A może jest to faktycznieC
obiekt nazywany polimorficznie, poprzez swójB
interfejs? 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.
źródło
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
Problem z diamentami :
źródło
someZ
i chce go rzucić,Object
a potem do niegoB
? KtóryB
to dostanie?Object
iz powrotem do tego typu ...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ć.
źródło
assert
?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.
źródło
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.
źródło
([..bool..]? "test": 1)
?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ł):
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.
źródło
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 .
źródło
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 ...
źródło
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:
Wydawałoby się, że
W.Test()
utworzenie instancji W powinno wywołać implementację metody wirtualnejFoo
zdefiniowanej wX
. 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: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. KiedyW.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) .źródło
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”:
W C ++ jest to całkowicie niemożliwe: jak najszybciej
F
iG
są połączone w jednej klasie, ichA
s są scalane też okres. Oznacza to, że nie może brać pod uwagę klasy bazowe nieprzezroczysty w C ++ (w tym przykładzie trzeba zbudowaćA
wH
tak trzeba wiedzieć, że obecny gdzieś w hierarchii). Jednak w innych językach może działać; na przykład,F
iG
mó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 ++):
Tutaj
B
używa tylko dziedziczenia wirtualnego. WięcE
zawiera dwa,B
które mają to samoA
. W ten sposób możesz uzyskaćA*
wskaźnik, który wskazuje naE
, ale nie możesz rzucić go naB*
wskaźnik, chociaż obiekt jest w rzeczywistościB
rzutowaniem niejednoznacznym, a tej niejednoznaczności nie można wykryć w czasie kompilacji (chyba że kompilator widzi cały program). Oto kod testowy:Ponadto implementacja może być bardzo złożona (zależy od języka; patrz odpowiedź Benjismitha).
źródło