Odpowiedzi:
Dziedziczenie wielokrotne (w skrócie MI) pachnie , co oznacza, że zwykle odbywało się to ze złych powodów i powróci w twarz opiekuna.
Dotyczy to dziedziczenia, a więc jeszcze bardziej dotyczy dziedziczenia wielokrotnego.
Czy Twój obiekt naprawdę musi dziedziczyć po innym? A Car
nie musi dziedziczyć z Engine
do pracy ani z Wheel
. A Car
ma Engine
i cztery Wheel
.
Jeśli używasz dziedziczenia wielokrotnego do rozwiązania tych problemów zamiast kompozycji, oznacza to, że zrobiłeś coś złego.
Zazwyczaj masz klasę A
, potem B
i C
zarówno dziedziczyć po A
. I (nie pytaj mnie dlaczego) ktoś zdecyduje, że D
musi dziedziczyć zarówno po, jak B
i C
.
Spotkałem się z tego rodzaju problemem dwa razy w ciągu 8 ośmiu lat i jest to zabawne z powodu:
D
nie powinien był odziedziczyć po obu B
i C
), bo to była zła architektura (właściwie C
nie powinna w ogóle istnieć ...)A
była obecna dwukrotnie w swojej klasie wnuków D
, a zatem aktualizacja jednego pola nadrzędnego A::field
oznaczała albo aktualizację go dwukrotnie (przez B::field
i C::field
), albo po cichu coś poszło nie tak i awarię później (nowy wskaźnik w B::field
i usuń C::field
...)Użycie słowa kluczowego virtual w C ++ do zakwalifikowania dziedziczenia pozwala uniknąć podwójnego układu opisanego powyżej, jeśli nie tego chcesz, ale w każdym razie, z mojego doświadczenia, prawdopodobnie robisz coś źle ...
W hierarchii obiektów powinieneś starać się zachować hierarchię jako drzewo (węzeł ma JEDNEGO rodzica), a nie jako wykres.
Prawdziwy problem z Diamentem Strachu w C ++ ( zakładając, że projekt jest dobry - poproś o sprawdzenie kodu! ), Polega na tym, że musisz dokonać wyboru :
A
występowała dwukrotnie w Twoim układzie i co to oznacza? Jeśli tak, to z całą pewnością odziedzicz po nim dwukrotnie.Ten wybór jest nieodłącznym elementem problemu, aw C ++, w przeciwieństwie do innych języków, można to zrobić bez dogmatów narzucających projekt na poziomie języka.
Ale jak wszystkie moce, z tą mocą wiąże się odpowiedzialność: poproś o sprawdzenie swojego projektu.
Wielokrotne dziedziczenie zera lub jednej konkretnej klasy i zera lub większej liczby interfejsów jest zwykle OK, ponieważ nie napotkasz Diamentu Strachu opisanego powyżej. W rzeczywistości tak się robi w Javie.
Zwykle to, co masz na myśli, gdy C dziedziczy po A
i B
to, że użytkownicy mogą używać C
tak, jakby to był A
i / lub tak, jakby był B
.
W C ++ interfejs jest klasą abstrakcyjną, która ma:
Dziedziczenie wielokrotne od zera do jednego rzeczywistego obiektu i zera lub większej liczby interfejsów nie jest uważane za „śmierdzące” (a przynajmniej nie tak bardzo).
Po pierwsze, wzorzec NVI może być użyty do stworzenia interfejsu, ponieważ rzeczywiste kryteria to brak stanu (tj. Brak zmiennych składowych , z wyjątkiem this
). Celem twojego abstrakcyjnego interfejsu jest opublikowanie umowy („możesz nazywać mnie tak i tak”), nic więcej, nic mniej. Ograniczeniem posiadania tylko abstrakcyjnej metody wirtualnej powinien być wybór projektu, a nie obowiązek.
Po drugie, w C ++ sensowne jest dziedziczenie wirtualne z abstrakcyjnych interfejsów (nawet z dodatkowymi kosztami / pośrednimi). Jeśli tego nie zrobisz, a dziedziczenie interfejsu pojawia się wielokrotnie w Twojej hierarchii, będziesz mieć niejasności.
Po trzecie, orientacja obiektu jest super, ale nie jest to jedyna prawda Out There TM w C ++. Używaj właściwych narzędzi i zawsze pamiętaj, że w C ++ masz inne paradygmaty, które oferują różnego rodzaju rozwiązania.
Czasem tak.
Zazwyczaj twoja C
klasa dziedziczy z A
i B
i A
i B
są dwóch niepowiązanych obiektów (czyli nie w tej samej hierarchii, nic wspólnego, różnych koncepcji, etc.).
Na przykład, możesz mieć system o Nodes
współrzędnych X, Y, Z, zdolny do wykonywania wielu obliczeń geometrycznych (być może punkt, część obiektów geometrycznych), a każdy Węzeł jest Automatycznym Agentem, zdolnym do komunikowania się z innymi agentami.
Być może masz już dostęp do dwóch bibliotek, z których każda ma własną przestrzeń nazw (kolejny powód, aby używać przestrzeni nazw ... Ale używasz przestrzeni nazw, prawda?), Jedna istota, geo
a drugaai
Więc masz własne own::Node
pochodzenie zarówno z, jak ai::Agent
i geo::Point
.
To jest moment, w którym powinieneś zadać sobie pytanie, czy zamiast tego nie powinieneś używać kompozycji. Jeśli own::Node
naprawdę jest zarówno a, jak ai::Agent
i a geo::Point
, to kompozycja nie wystarczy.
Wtedy będziesz potrzebować wielokrotnego dziedziczenia, aby own::Node
komunikować się z innymi agentami zgodnie z ich pozycją w przestrzeni 3D.
(Zauważysz, że ai::Agent
i geo::Point
jesteś całkowicie, całkowicie, całkowicie NIEZWIĄZANY ... To drastycznie zmniejsza niebezpieczeństwo wielokrotnego dziedziczenia)
Istnieją inne przypadki:
this
)Czasami możesz użyć kompozycji, a czasami MI jest lepsze. Chodzi o to, że masz wybór. Zrób to odpowiedzialnie (i poproś o sprawdzenie kodu).
Z mojego doświadczenia wynika, że przez większość czasu nie. MI nie jest odpowiednim narzędziem, nawet jeśli wydaje się działać, ponieważ może być używane przez leniwych do łączenia elementów bez zdawania sobie sprawy z konsekwencji (takich jak tworzenie Car
zarówno an, jak Engine
i a Wheel
).
Ale czasami tak. A wtedy nic nie będzie działać lepiej niż MI.
Ale ponieważ MI jest śmierdzący, przygotuj się na obronę swojej architektury w przeglądach kodu (a obrona jej jest dobra, ponieważ jeśli nie jesteś w stanie jej obronić, nie powinieneś tego robić).
Z wywiadu z Bjarne Stroustrupem :
Ludzie całkiem słusznie twierdzą, że nie potrzebujesz dziedziczenia wielokrotnego, ponieważ wszystko, co możesz zrobić z dziedziczeniem wielokrotnym, możesz również zrobić z dziedziczeniem pojedynczym. Po prostu użyj sztuczki delegowania, o której wspomniałem. Co więcej, w ogóle nie potrzebujesz dziedziczenia, ponieważ wszystko, co robisz z dziedziczeniem pojedynczym, możesz również zrobić bez dziedziczenia, przesyłając dalej przez klasę. Właściwie nie potrzebujesz też żadnych klas, ponieważ możesz to wszystko zrobić za pomocą wskaźników i struktur danych. Ale dlaczego chcesz to zrobić? Kiedy wygodnie jest korzystać z udogodnień językowych? Kiedy wolisz obejście? Widziałem przypadki, w których wielokrotne dziedziczenie jest przydatne, a nawet widziałem przypadki, w których dość skomplikowane wielokrotne dziedziczenie jest przydatne. Generalnie wolę korzystać z udogodnień oferowanych przez język, aby obejść ten problem
const
- musiałem pisać niezgrabne obejścia (zwykle przy użyciu interfejsów i kompozycji), gdy klasa naprawdę potrzebowała mutowalnych i niezmiennych wariantów. Jednakże, nigdy nie raz brakowało wielokrotne dziedziczenie, i nigdy nie czułem, że muszę napisać obejścia ze względu na brak tej funkcji. To jest różnica. W każdym przypadku, jaki kiedykolwiek widziałem, nie używanie MI jest lepszym wyborem projektowym, a nie obejściem.
Nie ma powodu, aby tego unikać i może być bardzo przydatne w sytuacjach. Musisz jednak zdawać sobie sprawę z potencjalnych problemów.
Największy z nich to diament śmierci:
class GrandParent;
class Parent1 : public GrandParent;
class Parent2 : public GrandParent;
class Child : public Parent1, public Parent2;
Masz teraz dwie „kopie” GrandParent in Child.
C ++ pomyślał o tym jednak i pozwala na wirtualne dziedziczenie w celu obejścia problemów.
class GrandParent;
class Parent1 : public virtual GrandParent;
class Parent2 : public virtual GrandParent;
class Child : public Parent1, public Parent2;
Zawsze sprawdzaj swój projekt, upewnij się, że nie używasz dziedziczenia, aby zaoszczędzić na ponownym wykorzystaniu danych. Jeśli możesz przedstawić to samo z kompozycją (a zazwyczaj możesz), jest to znacznie lepsze podejście.
GrandParent
w Child
. Istnieje strach przed MI, ponieważ ludzie po prostu myślą, że mogą nie rozumieć zasad językowych. Ale każdy, kto nie może zrozumieć tych prostych zasad, nie może również napisać nietrywialnego programu.
Zobacz w: wielokrotne dziedziczenie .
Dziedziczenie wielokrotne spotkało się z krytyką i jako takie nie jest wdrażane w wielu językach. Krytyka obejmuje:
- Zwiększona złożoność
- Niejednoznaczność semantyczna jest często podsumowywana jako problem diamentów .
- Brak możliwości jawnego wielokrotnego dziedziczenia z jednej klasy
- Kolejność dziedziczenia zmieniająca semantykę klas.
Wielokrotne dziedziczenie w językach z konstruktorami w stylu C ++ / Java zaostrza problem dziedziczenia konstruktorów i łańcuchów konstruktorów, tworząc w ten sposób problemy z utrzymaniem i rozszerzalnością w tych językach. Obiekty w relacjach dziedziczenia o bardzo różnych metodach konstrukcji są trudne do zaimplementowania w paradygmacie tworzenia łańcuchów konstruktorów.
Nowoczesny sposób rozwiązania tego problemu polega na użyciu interfejsu (klasa czysto abstrakcyjna), takiego jak interfejs COM i Java.
Mogę zamiast tego zrobić inne rzeczy?
Tak, możesz. Mam zamiar okraść GoF .
Dziedziczenie publiczne jest relacją IS-A i czasami klasa będzie rodzajem kilku różnych klas i czasami ważne jest, aby to odzwierciedlić.
Czasami przydatne są także „miksy”. Na ogół są to małe klasy, zwykle nie dziedziczące po niczym, zapewniające użyteczną funkcjonalność.
Tak długo, jak hierarchia dziedziczenia jest dość płytka (jak prawie zawsze powinna być) i dobrze zarządzana, jest mało prawdopodobne, że otrzymasz przerażające dziedzictwo diamentowe. Diament nie jest problemem we wszystkich językach, które używają wielokrotnego dziedziczenia, ale sposób jego traktowania w C ++ jest często niezręczny i czasami zagadkowy.
Chociaż spotkałem się z przypadkami, w których wielokrotne dziedziczenie jest bardzo przydatne, w rzeczywistości są one dość rzadkie. Jest to prawdopodobne, ponieważ wolę używać innych metod projektowania, gdy tak naprawdę nie potrzebuję dziedziczenia wielokrotnego. Wolę unikać mylących konstrukcji językowych i łatwo jest tworzyć przypadki dziedziczenia, w których trzeba naprawdę dobrze przeczytać podręcznik, aby dowiedzieć się, co się dzieje.
Nie należy „unikać” wielokrotnego dziedziczenia, ale należy być świadomym problemów, które mogą się pojawić, takich jak „problem z diamentami” ( http://en.wikipedia.org/wiki/Diamond_problem ) i ostrożnie traktować przekazaną władzę , tak jak powinieneś ze wszystkimi mocami.
Ryzykując nieco abstrakcyjności, myślę o dziedziczeniu w ramach teorii kategorii wydaje mi się pouczające.
Jeśli pomyślimy o wszystkich naszych klasach i strzałkach między nimi oznaczających relacje dziedziczenia, to coś takiego
A --> B
oznacza, że class B
pochodzi z class A
. Zauważ, że podane
A --> B, B --> C
mówimy, że C pochodzi od B, które pochodzi od A, więc mówi się również, że C pochodzi od A, a zatem
A --> C
Ponadto mówimy, że dla każdej klasy, z A
której A
wywodzi się trywialnie A
, nasz model dziedziczenia spełnia definicję kategorii. W bardziej tradycyjnym języku mamy kategorię Class
zawierającą obiekty, wszystkie klasy i morfizmy, relacje dziedziczenia.
To trochę konfiguracji, ale spójrzmy na nasz Diament Zagłady:
C --> D
^ ^
| |
A --> B
To podejrzany schemat, ale wystarczy. Więc D
dziedziczy wszystko A
, B
i C
. Co więcej, i coraz bliżej odpowiedzi na pytanie OP, D
dziedziczy również z dowolnej nadklasy A
. Możemy narysować diagram
C --> D --> R
^ ^
| |
A --> B
^
|
Q
Teraz problemy związane z Diamonda Śmierci oto kiedy C
i B
podzielić nieruchomość / nazwy metod i robi się niejednoznaczne; jeśli jednak przeniesiemy do tego jakieś wspólne zachowanie, A
niejednoznaczność znika.
Ujmując kategorycznie, chcemy A
, B
i C
być takim, że jeśli B
i C
dziedziczenie z Q
tego czasu A
może być przepisane jako podklasa Q
. To powoduje A
coś, co nazywa się wypychaniem .
Istnieje również symetryczna konstrukcja D
zwana pullback . Jest to w zasadzie najbardziej użyteczna klasa, jaką można skonstruować, która dziedziczy po obu B
i C
. Oznacza to, że jeśli masz jakąkolwiek inną klasę, R
dziedzicząc mnożenie po B
i C
, to D
jest to klasa, w której R
można ją przepisać jako podklasę D
.
Upewnienie się, że twoje wierzchołki diamentu są wycofane i wypchnięte, daje nam dobry sposób na ogólne radzenie sobie z konfliktami nazw lub problemami z konserwacją, które mogą wystąpić w przeciwnym razie.
Zauważ , że odpowiedź Paercebala zainspirowała to, ponieważ jego napomnienia wynikają z powyższego modelu, biorąc pod uwagę, że pracujemy w pełnej kategorii Klasa wszystkich możliwych klas.
Chciałem uogólnić jego argument na coś, co pokazuje, jak skomplikowane relacje wielokrotnego dziedziczenia mogą być zarówno potężne, jak i bezproblemowe.
TL; DR Pomyśl o relacjach dziedziczenia w swoim programie jako tworzących kategorię. Następnie możesz uniknąć problemów z Diamentem Zagłady, wykonując wypychania klas dziedziczonych wielokrotnie i symetrycznie, tworząc wspólną klasę nadrzędną, która jest wycofaniem.
Używamy Eiffla. Mamy doskonałe MI. Bez obaw. Nie ma problemów. Łatwe zarządzanie. Są chwile, aby NIE używać MI. Jest to jednak przydatne bardziej niż ludzie zdają sobie sprawę, ponieważ: A) posługują się niebezpiecznym językiem, który nie radzi sobie dobrze -LUB- B) są zadowoleni z tego, jak pracowali z MI przez lata -LUB- C) z innych powodów ( zbyt liczne, aby je wymienić, jestem pewien - zobacz odpowiedzi powyżej).
Dla nas używanie Eiffla, MI jest tak samo naturalne jak wszystko inne i jest kolejnym doskonałym narzędziem w zestawie narzędzi. Szczerze mówiąc, nie obchodzi nas, że nikt inny nie używa Eiffla. Bez obaw. Cieszymy się z tego, co mamy i zapraszamy do obejrzenia.
Kiedy patrzysz: zwróć szczególną uwagę na bezpieczeństwo Pustki i wyeliminowanie dereferencji wskaźnika zerowego. Kiedy wszyscy tańczymy dookoła MI, twoje wskazówki gubią się! :-)
Każdy język programowania ma nieco inne podejście do programowania obiektowego z zaletami i wadami. Wersja C ++ kładzie nacisk bezpośrednio na wydajność i ma towarzyszącą wadę, że pisanie nieprawidłowego kodu jest niepokojąco łatwe - i jest to prawdą w przypadku wielokrotnego dziedziczenia. W konsekwencji istnieje tendencja do odciągania programistów od tej funkcji.
Inne osoby zadały sobie pytanie, do czego dziedziczenie wielokrotne nie jest dobre. Ale widzieliśmy wiele komentarzy, które mniej więcej sugerują, że powodem, dla którego należy tego unikać, jest to, że nie jest to bezpieczne. Cóż, tak i nie.
Jak to często bywa w C ++, jeśli zastosujesz się do podstawowych wskazówek, możesz z nich bezpiecznie korzystać bez ciągłego „patrzenia przez ramię”. Główną ideą jest to, że rozróżnia się specjalny rodzaj definicji klasy zwany „mieszanką”; class jest mieszanką, jeśli wszystkie jej funkcje składowe są wirtualne (lub czysto wirtualne). Wtedy możesz dziedziczyć z jednej głównej klasy i tylu „miksów”, ile chcesz - ale powinieneś dziedziczyć mieszanki ze słowem kluczowym „wirtualny”. na przykład
class CounterMixin {
int count;
public:
CounterMixin() : count( 0 ) {}
virtual ~CounterMixin() {}
virtual void increment() { count += 1; }
virtual int getCount() { return count; }
};
class Foo : public Bar, virtual public CounterMixin { ..... };
Moja sugestia jest taka, że jeśli zamierzasz używać klasy jako klasy mieszanej, zastosuj także konwencję nazewnictwa, aby ułatwić każdemu czytającemu kod, aby zobaczyć, co się dzieje i sprawdzić, czy grasz zgodnie z zasadami podstawowych wytycznych . Przekonasz się, że działa to znacznie lepiej, jeśli twoje miksy mają również domyślne konstruktory, tylko ze względu na sposób działania wirtualnych klas podstawowych. I pamiętaj, aby wszystkie destruktory też były wirtualne.
Zwróć uwagę, że moje użycie tutaj słowa „mix-in” nie jest tym samym, co sparametryzowana klasa szablonu (zobacz ten link po dobre wyjaśnienie), ale myślę, że jest to uczciwe użycie terminologii.
Nie chcę teraz sprawiać wrażenia, że jest to jedyny sposób na bezpieczne korzystanie z dziedziczenia wielokrotnego. To tylko jeden sposób, który jest dość łatwy do sprawdzenia.
Powinieneś używać go ostrożnie, są takie przypadki, jak problem diamentów , kiedy sprawy mogą się skomplikować.
(źródło: learncpp.com )
Printer
nie powinno być nawet PoweredDevice
. A Printer
oznacza drukowanie, a nie zarządzanie energią. Implementacja określonej drukarki może wymagać zarządzania energią, ale te polecenia zarządzania energią nie powinny być ujawniane bezpośrednio użytkownikom drukarki. Nie mogę sobie wyobrazić żadnego rzeczywistego wykorzystania tej hierarchii.
Wykorzystywanie i nadużywanie dziedziczenia.
Artykuł świetnie wyjaśnia dziedziczenie i związane z nim niebezpieczeństwa.
Poza wzorem rombu wielokrotne dziedziczenie sprawia, że model obiektu jest trudniejszy do zrozumienia, co z kolei zwiększa koszty utrzymania.
Kompozycja jest z natury łatwa do zrozumienia, zrozumienia i wyjaśnienia. Pisanie kodu może być uciążliwe, ale dobre środowisko IDE (minęło kilka lat, odkąd pracowałem z Visual Studio, ale z pewnością wszystkie środowiska Java IDE mają świetne narzędzia automatyzujące skróty do kompozycji) powinny pokonać tę przeszkodę.
Jeśli chodzi o utrzymanie, „problem diamentów” pojawia się również w niedosłownych przypadkach dziedziczenia. Na przykład, jeśli masz A i B, a twoja klasa C rozszerza je oba, a A ma metodę `` makeJuice '', która wytwarza sok pomarańczowy i rozszerzasz ją, aby zrobić sok pomarańczowy z odrobiną limonki: co się stanie, gdy projektant B „dodaje metodę„ makeJuice ”, która generuje prąd elektryczny? „A” i „B” może być zgodny „rodzice” już teraz , ale to nie znaczy zawsze będą tak!
Ogólnie rzecz biorąc, maksyma polegająca na unikaniu dziedziczenia, a zwłaszcza dziedziczenia wielokrotnego, jest rozsądna. Jak wszystkie maksymy, są wyjątki, ale musisz upewnić się, że miga zielony neon wskazujący wszystkie wyjątki, które kodujesz (i trenuj swój mózg, aby za każdym razem, gdy zobaczysz takie drzewa dziedziczenia, rysujesz własnym migającym zielonym neonem znak) i od czasu do czasu sprawdzasz, czy wszystko ma sens.
what happens when the designer for 'B' adds a 'makeJuice' method which generates and electrical current?
Uhhh, oczywiście pojawia się błąd kompilacji (jeśli użycie jest niejednoznaczne).
Kluczową kwestią związaną z MI dla konkretnych obiektów jest to, że rzadko masz obiekt, który zgodnie z prawem powinien „być A i być B”, więc rzadko jest to poprawne rozwiązanie ze względów logicznych. Znacznie częściej masz obiekt C, który jest posłuszny „C może działać jako A lub B”, co można osiągnąć poprzez dziedziczenie i kompozycję interfejsu. Ale nie popełnij błędu - dziedziczenie wielu interfejsów to nadal MI, tylko jej podzbiór.
W szczególności w przypadku języka C ++ kluczową słabością tej funkcji nie jest faktyczne ISTNIENIE wielokrotnego dziedziczenia, ale niektóre konstrukcje, na które zezwala, są prawie zawsze źle sformułowane. Na przykład dziedziczenie wielu kopii tego samego obiektu, na przykład:
class B : public A, public A {};
jest zniekształcony WEDŁUG DEFINICJI. W tłumaczeniu na język angielski jest to „B to A i A”. Tak więc nawet w ludzkim języku istnieje poważna dwuznaczność. Czy chodziło Ci o „B ma 2 As” czy po prostu „B to A” ?. Dopuszczenie takiego patologicznego kodu, a co gorsza uczynienie go przykładem użycia, nie przyniosło C ++ żadnych korzyści, jeśli chodzi o argumentowanie za utrzymaniem tej funkcji w kolejnych językach.
Możesz użyć kompozycji zamiast dziedziczenia.
Ogólne wrażenie jest takie, że kompozycja jest lepsza i jest bardzo dobrze omówiona.
The general feeling is that composition is better, and it's very well discussed.
To nie znaczy, że kompozycja jest lepsza.
zajmuje 4/8 bajtów na zaangażowaną klasę. (Jeden ten wskaźnik na klasę).
To może nigdy nie być problemem, ale jeśli pewnego dnia będziesz mieć strukturę mikrodanych, która jest instancją miliardów czasu, to będzie.