A számítógép-programozásban a kettős metódus a multimetódus speciális típusa. Programtervezési minta, ami mechanizmusként működve függvényhíváskor szükség esetén két paramétere szerint választ konkrét meghívandó függvényt. A legtöbb objektumorientált rendszer ebben csak egy paramétert képes figyelembe venni. Ezt virtuális függvénynek vagy egyszerű metódusnak nevezik. Példák A kettős metódus hasznos azokban a helyzetekben, amikor a számítás az objektumok konkrét típusától függ. Használható például a következő helyzetekben: Vegyes objektumhalmaz rendezése. Objektumok egy listáját kell rendezni egy megadott kanonikus rendezés szerint. Mindkét típust ismerni kell ahhoz, hogy eldönthessük, hogy melyik melyik után következik. Adaptív ütközési algoritmusok különböző objektumok között. Erre példa egy játék, amiben másként ütközik egy űrhajó egy aszteroidához, mint egy űrállomáshoz.More Effective C++ by Scott Meyers(Addison-Wesley, 1996) Grafikai algoritmusok, amikben különbözőképpen kell megjeleníteni átfedéseket. Munkaközvetítő rendszerek, amelyekben különböző munkákat különböző személyek között osztanak ki, így a rendszer nem ad hozzá egy könyvelőt egy olyan munkához, ahova mérnök kell. Eseménykezelő rendszerek, ahol az esemény mellett a fogadó objektumot is ismerni kell. Kulcs-zár rendszerek, ahol egy kulcs több zárat is nyithat. Azt is tudni kell, hogy melyik kulcs melyik zárat nyitja. Megvalósítása A meghívandó függvény kiválasztása attól függ, hogy milyen argumentumokkal hívják a kettős metódust; emiatt a hívás plusz költséget jelent, többet, mint amennyivel az egyszerű metódus. C++-ban a dinamikus függvényhívás egyetlen eltolás kiszámítását jelenti, amit a fordító az objektum metódustáblája alapján statikusan számol. Egy kettős metódusokat támogató nyelvben ezt dinamikusan kell megtenni. Összehasonlítás a túlterheléssel Első ránézésre a kettős metódus a túlterhelés természetes eredményének tűnik. A túlterhelés ugyanúgy lehetővé teszi a függvény kiválasztását az argumentumok típusa alapján. Azonban túlterhelés esetén fordítás időben statikusan lesz választva a függvény, belső neveket használva, amely belső nevek magukban foglalják az argumentumok típusát. Például a foo(int) függvény belső neve __foo_i , és a foo(double) belső neve __foo_d. Ezzel a fordító elkerüli a névütközést, és a virtuális tábla használatát. Ezzel szemben a kettős metódusok hívásakor virtuális függvények hívódnak meg, és ehhez a virtuális táblában kell keresni.
Az alábbi példa túlterhelést használ. A cél ütközések megvalósítása egy játékban:
class SpaceShip {}; class ApolloSpacecraft : public SpaceShip {};
class Asteroid { public: virtual void CollideWith(SpaceShip&) { std::cout << "Asteroid hit a SpaceShip\n";
virtual void CollideWith(ApolloSpacecraft&) { std::cout << "Asteroid hit an ApolloSpacecraft\n";
class ExplodingAsteroid : public Asteroid { public: virtual void CollideWith(SpaceShip&) { std::cout << "ExplodingAsteroid hit a SpaceShip\n";
virtual void CollideWith(ApolloSpacecraft&) { std::cout << "ExplodingAsteroid hit an ApolloSpacecraft\n";
Ha most:
Asteroid theAsteroid; SpaceShip theSpaceShip; ApolloSpacecraft theApolloSpacecraft;
akkor a túlterhelés miatt ez a kód:
theAsteroid.CollideWith(theSpaceShip); theAsteroid.CollideWith(theApolloSpacecraft);
rendre ezt írja ki: Asteroid hit a SpaceShip és Asteroid hit an ApolloSpacecraft Továbbá, az
ExplodingAsteroid theExplodingAsteroid; theExplodingAsteroid.CollideWith(theSpaceShip); theExplodingAsteroid.CollideWith(theApolloSpacecraft);
kód futásának eredménye rendre ExplodingAsteroid hit a SpaceShip és ExplodingAsteroid hit an ApolloSpacecraft, szintén statikusan.
Ha az Asteroid közvetlen használat helyett referenciát kap, akkor dinamikusan lesz kiválasztva a metódus. Ez a kód Asteroid& theAsteroidReference = theExplodingAsteroid; theAsteroidReference.CollideWith(theSpaceShip); theAsteroidReference.CollideWith(theApolloSpacecraft);
kiírja, hogy ExplodingAsteroid hit a SpaceShip és ExplodingAsteroid hit an ApolloSpacecraft, úgy, ahogy vártuk. A következő kód azonban nem úgy működik, ahogy elvárnánk:
SpaceShip& theSpaceShipReference = theApolloSpacecraft; theAsteroid.CollideWith(theSpaceShipReference); theAsteroidReference.CollideWith(theSpaceShipReference);
Azt várnánk, hogy a függvény figyelembe veszi az argumentumok pontos típusát, vagyis az elvárt eredmény Asteroid hit an ApolloSpacecraft és ExplodingAsteroid hit an ApolloSpacecraft. Ehelyett azonban az jelenik meg, hogy Asteroid hit a SpaceShip és ExplodingAsteroid hit a SpaceShip. A problémát az jelenti, hogy a virtuális függvények kötése dinamikus, míg a túlterhelés statikus. Kettős metódus C++-ban A probléma megoldható a látogató programtervezési mintával. A fenti kódot kibővítjük a SpaceShip és a ApolloSpacecraft osztályokban a virtual void CollideWith(Asteroid& inAsteroid) { inAsteroid.CollideWith(*this); } függvénnyel. A korrekt működéshez a hívó kódban a következőt kell tenni: SpaceShip& theSpaceShipReference = theApolloSpacecraft; Asteroid& theAsteroidReference = theExplodingAsteroid; theSpaceShipReference.CollideWith(theAsteroid); theSpaceShipReference.CollideWith(theAsteroidReference);
Ez most már azt írja, amit kell: Asteroid hit an ApolloSpacecraft és ExplodingAsteroid hit an ApolloSpacecraft A kulcs az, hogy theSpaceShipReference.CollideWith(theAsteroidReference); a következőképpen alakul: theSpaceShipReference referencia, a fordító a vtable-ben keresi a metódust. Ekkor hívja a ApolloSpacecraft::CollideWith(Asteroid&) metódust. Az ApolloSpacecraft::CollideWith(Asteroid&) metódusban újra referencia az inAsteroid, így az inAsteroid.CollideWith(*this) eredménye egy újabb keresés a vtable-ben. Ekkor inAsteroid referencia az ExplodingAsteroid-ra, így végül ExplodingAsteroid::CollideWith(ApolloSpacecraft&) hívódik meg. Kettős metódus C#-ban C#-ban nincs szükség látogatóra kettős metódusokhoz. Elérhető polimorfizmussal, a megfelelő argumentumot dynamicnak jelölve. A futás idejű kötő kiválasztja a megfelelő túlterhelést futás időben. A döntés figyelembe veszi a példány futásidejű típusát és az argumentum futásidejű típusát. Kettős metódus Eiffelben Az Eiffelben használhatók az ágensek a kettős metódusok bevezetésére. A későbbi példa is ezt a nyelvi szerkezetet használja.
A megoldandó feladata egy rajzprogram készítése, amikkel alakzatokat (SHAPE) lehet rajzolni párhuzamosan több felületre (SURFACE). Mindkettő egyaránt ismer egy draw függvényt, de egymásról nem tudnak. Szeretnénk, ha a kettő kapcsolatba lépne egymással kovariáns módon. Egy polimorf felületet rá kell bírni arra, hogy kirajzoljon magára egy polimorf alakzatot. Kimeneti példa A kimeneti példa most szövegként adja vissza, hogy mikor melyik felületre melyik rajzelemet rajzolja ki. Az alábbi eredményekhez két SURFACE látogatót adnak át polimorf SHAPE objektumok listájának. A látogató nincs tisztában a pontos típusokkal. Ehelyett a kód futás idejű polimorfizmusra és az ágensek módszerére hagyatkozik, hogy nagy rugalmasságot érjen el a két absztrakt (deferred) osztály és leszármazottaik között.
draw a red POLYGON on ETCHASKETCH draw a red POLYGON on GRAFFITI_WALL draw a grey RECTANGLE on ETCHASKETCH draw a grey RECTANGLE on GRAFFITI_WALL draw a green QUADRILATERAL on ETCHASKETCH draw a green QUADRILATERAL on GRAFFITI_WALL draw a blue PARALLELOGRAM on ETCHASKETCH draw a blue PARALLELOGRAM on GRAFFITI_WALL draw a yellow POLYGON on ETCHASKETCH draw a yellow POLYGON on GRAFFITI_WALL draw a purple RECTANGLE on ETCHASKETCH draw a purple RECTANGLE on GRAFFITI_WALL Látogató minta A látogató objektum polimorf adatszerkezetet jár be polimorf módon, és csinál valamit az egyes objektumokkal. Ez lehet függvényhívás vagy ágens használata is. Példánkban az adatszerkezet SHAPE objektumok listája, aminek elemeit egy SURFACE objektummal látogatja meg, és kirajzoltatja őket a SURFACE objektumra.
make -- Print shapes on surfaces. local l_shapes: ARRAYED_LIST [SHAPE] l_surfaces: ARRAYED_LIST [SURFACE] do create l_shapes.make (6) l_shapes.extend (create {POLYGON}.make_with_color ("red")) l_shapes.extend (create {RECTANGLE}.make_with_color ("grey")) l_shapes.extend (create {QUADRILATERAL}.make_with_color ("green")) l_shapes.extend (create {PARALLELOGRAM}.make_with_color ("blue")) l_shapes.extend (create {POLYGON}.make_with_color ("yellow")) l_shapes.extend (create {RECTANGLE}.make_with_color ("purple"))
create l_surfaces.make (2) l_surfaces.extend (create {ETCHASKETCH}.make) l_surfaces.extend (create {GRAFFITI_WALL}.make)
across l_shapes as ic_shapes loop across l_surfaces as ic_surfaces loop ic_surfaces.item.drawing_agent (ic_shapes.item.drawing_data_agent) end end end
Az adatszerkezetek létrehozásával kezdjük. Végigiterálunk az egyik listán (SHAPE), és megengedjük, hogy a másik lista elemei sorra meglátogassák őket. A példa kódban SURFACE objektumok látogatják a SHAPE objektumokat.
A kód indirekt, a drawing_agent által polimorf módon hívja a {SURFACE}.draw metódust, és az első hívás a kettős metódusban. Átad egy indirekt és polimorf ágenst, ez a drawing_data_agent; emiatt a látogatónak elég a következőket tudnia: a SURFACE kirajzolásának ágensét a SHAPE rajzolási adat ágensét
Mivel a SHAPE és a SURFACE is definiálja ágenseit, a látogató kódnak nem kell tudnia, hogy mi lesz a hívás eredménye, polimorf lesz-e, vagy más. Ez az indirekció és kibontás ismeretlen a fontosabb nyelvekben, mint C, C++ és Java, habár csinálható hasonló reflexióval vagy túlterheléssel. SURFACE A {SURFACE}.draw polimorf hívásával egy ágens hívódik meg, ami a második polimorf hívás a mintában.
deferred class SURFACE
feature {NONE} -- Initialization
make -- Initialize Current. do drawing_agent := agent draw end
feature -- Access
drawing_agent: PROCEDURE [ANY, TUPLE [STRING, STRING]] -- Drawing agent of Current.
feature {NONE} -- Implementation
draw (a_data_agent: FUNCTION [ANY, TUPLE, TUPLE [name, color: STRING]]) -- Draw `a_shape' on Current. local l_result: TUPLE [name, color: STRING] do l_result := a_data_agent (Void) print ("draw a " + l_result.color + " " + l_result.name + " on " + type + "%N") end
type: STRING -- Type name of Current. deferred end
end
A 19. sor ágens argumentuma és ennek hívása a 24. sorban is polimorf. Az ágens lebont, mivel a {SURFACE}.draw semmit sem tud arról, hogy az a_data_agent melyik osztályon alapul. Így nem lehet megmondani, hogy a művelet ágense melyik osztályból való, nem kell feltétlenül a SHAPE osztály vagy leszármazottainak elemének lennie. Ez az Eiffel ágenseinek előnye a más nyelvekben használt egyszeres örökléssel, dinamikus és polimorf kötéseivel szemben.
Az ágens futás időben dinamikusan polimorf, mivel az objektum akkor jön létre, amikor létre kell jönnie, és mivel az objektummá tett rutint akkor határozza meg a környezet. Az egyedüli erős kötés tudás az ágens szignatúrájának Result típusa, ami egy két elemű TUPLE. Ez a specifikus követelmény a tartalmazó metódus (feature) kérésén alapul, ami szükséges, és nem kell elkerülni, talán nem is kerülhető el. Például a 25. sor a TUPLE elnevezett elemeit használja, hogy megfeleljen a SURFACE draw metódusának.
Jegyezzük meg, hogy csak a drawing_agent láthatósága nyilvános, azaz csak ezt exportálták ANY kliensnek! Ez azt jelenti, hogy a látogató, ami az osztály egyedüli kliense csak erről kell, hogy tudjon, hogy elvégezhesse feladatát. SHAPE A SHAPE osztály a kirajzolandó osztályok őse; tartalmazza a kirajzoláshoz szükséges adatokat. A rajzolás végezhető SURFACE objektumon, de arra nincs feltétlenül szükség. Az ágensek ismét biztosítják az indirekciót, és a nem tudást az osztályról, amire szükség van a kovariáns működéshez.