A többszörös öröklődés olyan funkció néhány objektumorientált programozási nyelvben, amely lehetővé teszi, hogy egy objektum vagy osztály több szülőobjektumtól vagy szülőosztálytól örökölje a tulajdonságokat. Ez különbözik az egyszeres öröklődéstől, ahol egy objektum vagy osztály csak egyetlen meghatározott objektumtól vagy osztálytól örökölhet.

A többszörös öröklődés évek óta vitatott kérdés,[1][2] mivel az ellenzők rámutatnak a megnövekedett komplexitásra és a kétértelműségekre, például a "gyémánt probléma" esetében, amikor nem egyértelmű, hogy melyik szülőosztályból öröklődik egy adott tulajdonság, ha több szülőosztály is megvalósítja azt. Ezt különböző módokon lehet kezelni, például virtuális öröklődéssel.[3] Az öröklődésen alapuló objektum összetétel alternatív módszereit, mint például a mixinek és a trait-ek, szintén javasolták a kétértelműségek kezelésére.

Részletek

szerkesztés

Az objektumorientált programozásban (OOP) az öröklődés két osztály közötti kapcsolatot ír le, amelyben az egyik osztály (a gyermekosztály ) alosztálya a szülőosztálynak. A gyerek örökli a szülő metódusait és attribútumait, lehetővé téve a közös funkcionalitást. Például létrehozhatunk egy Emlős nevű változó osztályt olyan tulajdonságokkal, mint az evés, szaporodás stb.; majd definiálhatunk egy Macska nevű gyerekosztályt, amely örökli ezeket a tulajdonságokat anélkül, hogy explicit módon kellene azokat megprogramozni, miközben új tulajdonságokat is hozzáad, mint például az egerek üldözése.

A többszörös öröklődés lehetővé teszi a programozók számára, hogy egynél több teljesen ortogonális hierarchiát használjanak egyszerre, például lehetővé teszi, hogy a Macska örökölje a rajzfilmfiguráktól, a Háziállatot és az Emlősöket, és az összes ilyen osztályból elérje a funkciókat.

Megvalósítások

szerkesztés

A többszörös öröklődést támogató nyelvek a következők: C++, Common Lisp, EuLisp, Curl, Dylan, Eiffel, Logtalk, Object REXX, Scala, OCaml, Perl, POP-11, Python, R, Raku és Tcl[4][5].

Az IBM rendszerobjektum modell (SOM: IBM System Object Model) futási utási ideje támogatja a többszörös öröklést, és bármely SOM-ot megcélzó programozási nyelv képes több bázisból örökölt új SOM-osztályok megvalósítására.

Néhány objektumorientált nyelv, például a Swift, a Java, a Fortran 2003-as verziója óta, a C# és a Ruby egyszeres öröklődést valósítanak meg, bár a protokollok vagy interfészek a valódi többszörös öröklődés néhány funkcióját biztosítják.

A PHP tulajdonság osztályokat használ bizonyos metódusmegvalósítások öröklésére. A Ruby modulokat használ több metódus öröklésére.

A gyémánt probléma

szerkesztés
 
Egy gyémánt osztály öröklődési diagram

A "gyémánt probléma" (néha "halálos gyémánt" (angolul: "Deadly Diamond of Death"[6]) néven is ismert) egy kétértelműség, amely akkor merül fel, amikor két osztály, B és C, örököl A-ból, és a D osztály mind B-ből, mind C-ből örököl. Ha van egy metódus A-ban, amelyet B és C felülírt, de D nem írja felül azt, akkor melyik verzióját örökli D: B-ét vagy C-ét?

Például a GUI szoftverfejlesztés kontextusában egy Button osztály örökölhet mind a Rectangle osztályból (megjelenés miatt), mind a Clickable osztályból (funkcionalitás/bemenet kezelés miatt), és a Rectangle és a Clickable osztályok mind az Object osztályból örökölnek. Most, ha az equals metódust hívják meg egy Button objektum esetében, és nincs ilyen metódus a Button osztályban, de van felülírt equals metódus a Rectanglevagy a Clickable osztályban (vagy mindkettőben), akkor melyik metódust kell végül meghívni?

Ezt "gyémánt problémának" nevezik, mert az osztályöröklési diagram alakja ilyen helyzetben gyémánt formájú. Ebben az esetben az A osztály van a tetején, alatta külön-külön a B és C osztályok, majd D alul egyesíti a kettőt, így gyémánt alakot formálva.

Enyhítés

szerkesztés

A nyelveknek eltérő módszereik vannak a megismételt öröklődés problémáinak kezelésére.

  • A C# (C# 8.0-tól kezdve) lehetővé teszi az alapértelmezett interfész metódusmegvalósítást, ami azt eredményezi, hogy egy A osztály, amely implementálja az Ia és Ib interfészeket hasonló metódusokkal, amelyek alapértelmezett megvalósítással rendelkeznek, két "örökölt" metódussal rendelkezhet ugyanazzal a szignatúrával, és ezzel a gyémánt problémát okozza. Ezt vagy azáltal lehet enyhíteni, hogy az A-t kötelezővé tesszük a metódus saját implementálására, ezáltal elkerülve az egyértelműséget, vagy kényszerítjük a hívót arra, hogy először átkonvertálja az A objektumot a megfelelő interfészre annak a metódusnak az alapértelmezett implementációjának használatához (pl. ((Ia) aInstance).Method();.
  • A C++ alapértelmezetten minden öröklési utat külön követ, tehát egy D objektum valójában két külön A objektumot tartalmaz, és az A tagok használata megfelelően ki kell egészíteni. Ha az öröklés A-tól B-ig és az öröklés A-tól C-ig mindkettő "virtual" (például "class B : virtual public A"), akkor a C++ különleges figyelmet fordít arra, hogy csak egy A objektumot hozzon létre, és az A tagok használata helyesen működik. Ha a virtuális öröklést és a nem virtuális öröklést összekeverik, akkor egyetlen virtuális A van, és egy nem virtuális A van minden nem virtuális öröklési úton az A-hoz. A C++-ban ki kell egyértelműen jelölni, hogy melyik szülőosztályból hívják meg az adott tulajdonságot, azaz Worker::Human.Age. A C++ nem támogatja az explicit ismétlődő öröklést, mivel nem lenne mód arra, hogy melyik szuperosztályt kell használni (például egy osztály többször is megjelenhet egyetlen származási listában [class Dog : public Animal, Animal]). A C++ továbbá lehetővé teszi a többszörös osztály egyetlen példányának létrehozását a virtuális öröklés mechanizmusán keresztül (például Worker::Human és Musician::Human ugyanarra az objektumra fog hivatkozni).
  • A Common Lisp CLOS megpróbálja biztosítani mind a megfelelő alapértelmezett viselkedést, mind annak felülbírálásának lehetőségét. Alapértelmezetten egyszerűen fogalmazva a módszerek D,B,C,A, sorrendben vannak rendezve, amikor B előbb szerepel a C osztálydefinícióban. A legspecifikusabb argumentumosztályokkal rendelkező módszer lesz kiválasztva (D>(B,C)>A); majd a szülőosztályok sorrendjében, ahogyan azokat a részosztály definíciójában megnevezték (B>C). Azonban a programozó felülírhatja ezt, megadva egy konkrét módszerfeloldási sorrendet vagy szabályt a módszerek kombinálására. Ezt metódus kombinációnak nevezik, amely teljes mértékben ellenőrizhető. A MOP (metatárgy protokoll) továbbá lehetőséget biztosít az öröklődés, a dinamikus kiküldés, az osztály példányosítása és más belső mechanizmusok módosítására anélkül, hogy az rendszer stabilitását befolyásolná.
  • A Curl csak azokat az osztályokat engedi, amelyeket egyértelműen megjelöltek közöseknek, hogy ismétlődően öröklődhessenek. A közös osztályoknak minden rendes konstruktorhoz meg kell határozniuk egy másodlagos konstruktort az osztályban. A rendes konstruktort akkor hívják meg először, amikor a közös osztály állapota egy részosztály konstruktorán keresztül inicializálódik, és a másodlagos konstruktor minden más részosztályhoz lesz hívva.
  • Az Eiffelben az ősosztályok tulajdonságai explicit módon választhatók ki a "select" és "rename" direktívákkal. Ez lehetővé teszi az alap osztály tulajdonságainak megosztását az örököseik között, vagy mindegyiknek külön példányt ad a bázis osztályból. Az Eiffel lehetővé teszi az örökölt tulajdonságok explicit összekapcsolását vagy szétválasztását is. Az Eiffel automatikusan összekapcsolja a tulajdonságokat, ha azok azonos nevet és megvalósítást kapnak. Az osztály író választhatja meg az örökölt tulajdonságok átnevezését azok szétválasztásához. A többszörös öröklődés gyakori jelenség az Eiffel fejlesztésben; például a legtöbb hatékony osztály a széles körben használt EiffelBase adatszerkezetek és algoritmusok könyvtárában két vagy több szülőt használ.[7]
  • A Go a fordítási időben megakadályozza a gyémánt problémát. Ha egy D struktúra két B és C struktúrát ágyaz be, amelyek mindegyike rendelkezik egy F() nevű módszerrel, és ezzel kielégíti az A interfészt, akkor a fordító "kétértelmű szelektor" hibával fog panaszkodni, ha D.F() hívódik meg, vagy ha egy D példányt egy A típusú változóhoz rendelnek. A. B és C módszerei explicit módon hívhatók a D.B.F() vagy a D.C.F() módon.
  • A Java 8 bevezeti az alapértelmezett metódusokat az interfészekben. Ha A,B és C interfészek, akkor B és C mindegyike különböző megvalósítást adhat egy A absztrakt módszeréhez, ami a gyémánt problémát okozza. Vagy a D osztálynak újra kell megvalósítania a módszert (amelynek törzsét egyszerűen átirányíthatja a hívást az egyik szülő megvalósítására), vagy az egyértelműséget fordítási hibaként elutasítják[8]. A Java 8 előtt a Java nem volt kitéve a gyémánt probléma kockázatának, mert nem támogatta a többszörös öröklődést, és az interfész alapértelmezett metódusai nem voltak elérhetők.
  • A JavaFX Script 1.2 verziója mixinek használatával teszi lehetővé a többszörös öröklődést. Konfliktus esetén a fordító megtiltja a kétértelmű változók vagy függvények közvetlen használatát. Minden örökölt tagot továbbra is elérhetünk az objektum átalakításával az érdeklődési mixin-re, például (individual as Person).printInfo();.
  • A Kotlin lehetővé teszi az interfészek többszörös öröklését. Azonban egy gyémánt probléma forgatókönyvében a gyerekosztálynak felül kell írnia azt a módszert, amely okozza az öröklési konfliktust, és meg kell adnia, hogy melyik szülőosztály implementációját kell használni. például super<ChosenParentInterface>.someMethod()
  • A Logtalk támogatja mind az interfész, mind az implementáció többszörös öröklődését, lehetővé téve a módszernelnevek deklarálását, amelyek mind az átnevezést, mind a módszerek hozzáférhetőségét biztosítják, és elkerülik az alapértelmezett konfliktusfeloldási mechanizmus által elrejtett módszereket.
  • Az OCaml-ben a szülőosztályokat külön-külön határozzák meg az osztálydefiníció testében. A módszerek (és attribútumok) ugyanabban a sorrendben öröklődnek, és minden újonnan örökölt módszer felülírja a meglévő módszereket. Az OCaml az osztályöröklési lista utolsó egyező definícióját választja ki a módszer megvalósításának eldöntéséhez a kétértelműségek esetén. Az alapértelmezett viselkedés felülbírálásához egyszerűen meg kell adni a kívánt osztálydefiníciót a módszerhívás során.
  • A Perl a szülőosztályok listáját rendezett listaként használja az öröklődéshez. A fordító az első módszert használja, amelyet a szuperosztályok listájának mélységi keresésével talál meg, vagy a osztály hierarchiájának C3 linearizációját használva. Különböző kiterjesztések alternatív osztálykompozíciós sémaákat biztosítanak. Az öröklés sorrendje befolyásolja az osztály szemantikáját. A fenti kétértelműség esetén a B osztály és annak ősai ellenőrizve lesznek a C osztály és annak ősai előtt, így az A módszer B-n keresztül öröködik. Ez megosztott Io és Picolisppel. A Perlben ezt a viselkedést felül lehet írni a mro vagy más modulok használatával a C3 linearizáció vagy más algoritmusok használatával.[9]
  • A Python ugyanazt a struktúrát használja, mint a Perl, de ellentétben a Perllal, belefoglalja a nyelv szintaxisába. Az öröklés sorrendje befolyásolja az osztály szemantikáját. A Pythonnak meg kellett küzdenie ezzel a problémával az új stílusú osztályok bevezetésekor, amelyek mindegyikének van egy közös ősük, az object. A Python egy osztálylistát hoz létre a C3 linearizációs (vagy MRO - Method Resolution Order) algoritmus segítségével. Ez az algoritmus két kényszer betartását erőszakolja: a gyerekek előzik meg a szülőket, és ha egy osztály több osztályból örököl, azokat az osztályokat tartják meg a base classes tuple-ban megadott sorrendben (azonban ebben az esetben néhány osztály a hierarchiában magasabb helyen lehet, mint az alacsonyabbak[10]). Tehát a módszer feloldási sorrendje: D, B, C, A.[11]
  • A Ruby osztályoknak pontosan egy szülőjük van, de több modulból is örökölhetnek; a Ruby osztálydefiníciók végrehajtódnak, és egy módszer (újra)definiálása elrejti az összes korábbi meghatározást a végrehajtás idején. A futásidejű metaprogramozás hiányában ez közel azonos szemantikával rendelkezik, mint a jobboldali mélységi keresés.
  • A Scala lehetővé teszi a trait-ek többszörös beállítását, ami lehetővé teszi a többszörös öröklődést a osztály- és trait-hierarchia közötti megkülönböztetés hozzáadásával. Egy osztály csak egy osztályból örökölhet, de annyi traitet keverhet be, amennyit csak szeretne. A Scala a módszerek neveinek feloldását a kiterjesztett "trait"-ek jobb-bal mélységi keresésével oldja fel, mielőtt az eredményül kapott listában minden modul kivételével az utolsó előfordulást eltávolítaná. Tehát a feloldási sorrend: [D, C, A, B, A], ami lecsökken [D, C, B, A] -re.
  • A Tcl több szülőosztályt is engedélyez; a szülőosztályok sorrendje az osztálydeklarációban befolyásolja a tagok nevének feloldását a C3 linearizációs algoritmus használatával.[12]

Azok a nyelvek, amelyek csak egyetlen öröklődést engedélyeznek, ahol egy osztály csak egy alap osztályból származhat, nem rendelkeznek gyémánt problémával. Ennek az az oka, hogy az ilyen nyelvekben legfeljebb egy implementáció van bármelyik módszerre bármely szinten az öröklési láncban, függetlenül a módszerek ismétlődésétől vagy helyétől. Általában ezek a nyelvek lehetővé teszik az osztályok számára, hogy több protokollt, úgynevezett interfészeket implementáljanak, például a Java-ban. Ezek a protokollok módszereket definiálnak, de nem biztosítanak konkrét implementációkat. Ezt a stratégiát használta az ActionScript, a C#, a D, a Java, a Nemerle, az Object Pascal, az Objective-C, a Smalltalk, a Swift és a PHP[13]. Ezek a nyelvek mind engedélyezik az osztályoknak, hogy több protokollt implementáljanak.

Továbbá, az Ada, a C#, a Java, az Object Pascal, az Objective-C, a Swift és a PHP lehetővé teszi az interfészek többszörös öröklődését (amit protokolloknak neveznek az Objective-C-ben és a Swiftben). Az interfészek olyan absztrakt alap osztályokhoz hasonlóak, amelyek módszer aláírásokat határoznak meg anélkül, hogy bármilyen viselkedést megvalósítanának. ("Tiszta" interfészek, mint például azok a Java-ban egészen a 7. verzióig, nem engedélyezik az implementációt vagy az példányadatokat az interfészen.) Mindazonáltal, amikor több interfész deklarálja ugyanazt a módszer aláírást, amint a módszert valahol az öröklési láncban megvalósítják (definiálják), ez felülbírálja a fentebb az láncban lévő bármely módszer implementációját (superclass-ekben). Ezért az öröklési lánc bármely adott szintjén legfeljebb egy módszer implementációja lehet. Így a gyémánt probléma nem jelentkezik az egyetlen öröklési módszer implementációjában, még akkor sem, ha többszörös interfészöröklődés van. A Java 8 és a C# 8 interfészekhez alapértelmezett implementáció bevezetésével még mindig lehetséges a Gyémánt Probléma generálása, bár ez csak fordítási időben jelentkezik hibaként.

  1. Cargill (Winter 1991). „Controversy: The Case Against Multiple Inheritance in C++”. Computing Systems 4, 69–82. o.  
  2. Waldo (Spring 1991). „Controversy: The Case For Multiple Inheritance in C++”. Computing Systems 4, 157–171. o.  
  3. Schärli: Traits: Composable Units of Behavior (PDF). Web.cecs.pdx.edu. (Hozzáférés: 2016. október 21.)
  4. incr Tcl. blog.tcl.tk. (Hozzáférés: 2020. április 14.)
  5. Introduction to the Tcl Programming Language. www2.lib.uchicago.edu. (Hozzáférés: 2020. április 14.)
  6. Martin, Robert C.: Java and C++: A critical comparison (PDF). Objectmentor.com , 1997. március 9. [2005. október 24-i dátummal az eredetiből archiválva]. (Hozzáférés: 2016. október 21.)
  7. Standard ECMA-367. Ecma-international.org. (Hozzáférés: 2016. október 21.)
  8. State of the Lambda. Cr.openjdk.java.net . (Hozzáférés: 2016. október 21.)
  9. perlobj. perldoc.perl.org. (Hozzáférés: 2016. október 21.)
  10. Abstract: The Python 2.3 Method Resolution Order. Python.org . (Hozzáférés: 2016. október 21.)
  11. Unifying types and classes in Python 2.2. Python.org. (Hozzáférés: 2016. október 21.)
  12. Manpage of class. Tcl.tk, 1999. november 16. (Hozzáférés: 2016. október 21.)
  13. Object Interfaces - Manual. PHP.net , 2007. július 4. (Hozzáférés: 2016. október 21.)

Fordítás

szerkesztés

Ez a szócikk részben vagy egészben a Multiple inheritance című angol Wikipédia-szócikk ezen változatának fordításán alapul. Az eredeti cikk szerkesztőit annak laptörténete sorolja fel. Ez a jelzés csupán a megfogalmazás eredetét és a szerzői jogokat jelzi, nem szolgál a cikkben szereplő információk forrásmegjelöléseként.

További irodalom

szerkesztés

További információk

szerkesztés