Kettős metódus

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.[1]
  • 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.[2] 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 ETCHASKETCHdraw a red POLYGON on GRAFFITI_WALLdraw a grey RECTANGLE on ETCHASKETCHdraw a grey RECTANGLE on GRAFFITI_WALLdraw a green QUADRILATERAL on ETCHASKETCHdraw a green QUADRILATERAL on GRAFFITI_WALLdraw a blue PARALLELOGRAM on ETCHASKETCHdraw a blue PARALLELOGRAM on GRAFFITI_WALLdraw a yellow POLYGON on ETCHASKETCHdraw a yellow POLYGON on GRAFFITI_WALLdraw a purple RECTANGLE on ETCHASKETCHdraw 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.locall_shapes: ARRAYED_LIST [SHAPE]l_surfaces: ARRAYED_LIST [SURFACE]docreate 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 loopacross l_surfaces as ic_surfaces loopic_surfaces.item.drawing_agent (ic_shapes.item.drawing_data_agent)endendend

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 classSURFACEfeature {NONE} -- Initializationmake-- Initialize Current.dodrawing_agent := agent drawendfeature -- Accessdrawing_agent: PROCEDURE [ANY, TUPLE [STRING, STRING]]-- Drawing agent of Current.feature {NONE} -- Implementationdraw (a_data_agent: FUNCTION [ANY, TUPLE, TUPLE [name, color: STRING]])-- Draw `a_shape' on Current.locall_result: TUPLE [name, color: STRING]dol_result := a_data_agent (Void)print ("draw a " + l_result.color + " " + l_result.name + " on " + type + "%N")endtype: STRING-- Type name of Current.deferred endend

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.

Jegyezzük meg továbbá, hogy a SHAPE osztály csak a drawing_data_agent elemet teszi nyilvánossá a kliensek számára. Így a létrehozás mellett ez az egyetlen mód, ahogy egy SHAPE objektummal kapcsolatba lehet lépni, amit bármely kliens indirekt használ, és polimorfikusan szerez adatokat a SHAPE-ről!

deferred classSHAPEfeature {NONE} -- Initializationmake_with_color (a_color: like color)-- Make with `a_color' as `color'.docolor := a_colordrawing_data_agent := agent drawing_dataensurecolor_set: color.same_string (a_color)endfeature -- Accessdrawing_data_agent: FUNCTION [ANY, TUPLE, like drawing_data]-- Data agent for drawing.feature {NONE} -- Implementationdrawing_data: TUPLE [name: like name; color: like color]-- Data needed for drawing of Current.doResult := [name, color]endname: STRING-- Object name of Current.deferred endcolor: STRING-- Color of Current.end

Klasszikus űrhajó példa

A klasszikus űrhajós példa egyik változatában űrhajók, aszteroidák és űrállomások szerepelnek. Egy kettős metódus kezelheti a találkozásokat két kovariáns objektum között. Példánkban a két űrhajó USS Enterprise és USS Excelsior.

Egy kimenet:

Starship Enterprise changes position from A-001 to A-002.Starship Enterprise takes evasive action, avoiding Asteroid `Rogue 1'!Starship Enterprise changes position from A-002 to A-003.Starship Enterprise takes evasive action, avoiding Asteroid `Rogue 2'!Starship Enterprise beams a science team to Starship Excelsior as they pass!Starship Enterprise changes position from A-003 to A-004.Starship Excelsior changes position from A-003 to A-005.Starship Enterprise takes evasive action, avoiding Asteroid `Rogue 3'!Starship Excelsior is near Space Station Deep Space 9 and is dockable.Starship Enterprise changes position from A-004 to A-005.Starship Enterprise beams a science team to Starship Excelsior as they pass!Starship Enterprise is near Space Station Deep Space 9 and is dockable.

Látogató

A látogató ebben a példában is kettős metódust valósítanak meg:

make-- Allow SPACESHIP objects to visit and move about in a universe.locall_universe: ARRAYED_LIST [SPACE_OBJECT]l_enterprise,l_excelsior: SPACESHIPdocreate l_enterprise.make_with_name ("Enterprise", "A-001")create l_excelsior.make_with_name ("Excelsior", "A-003")create l_universe.make (0)l_universe.force (l_enterprise)l_universe.force (create {ASTEROID}.make_with_name ("Rogue 1", "A-002"))l_universe.force (create {ASTEROID}.make_with_name ("Rogue 2", "A-003"))l_universe.force (l_excelsior)l_universe.force (create {ASTEROID}.make_with_name ("Rogue 3", "A-004"))l_universe.force (create {SPACESTATION}.make_with_name ("Deep Space 9", "A-005"))visit (l_enterprise, l_universe)l_enterprise.set_position ("A-002")visit (l_enterprise, l_universe)l_enterprise.set_position ("A-003")visit (l_enterprise, l_universe)l_enterprise.set_position ("A-004")l_excelsior.set_position ("A-005")visit (l_enterprise, l_universe)visit (l_excelsior, l_universe)l_enterprise.set_position ("A-005")visit (l_enterprise, l_universe)endfeature {NONE} -- Implementationvisit (a_object: SPACE_OBJECT; a_universe: ARRAYED_LIST [SPACE_OBJECT])-- `a_object' visits `a_universe'.doacross a_universe as ic_universe loopcheck attached {SPACE_OBJECT} ic_universe.item as al_universe_object thena_object.encounter_agent.call ([al_universe_object.sensor_data_agent])endendend

A kettős metódus feloldását a 35. sorban láthatjuk, ahol két ágens indirekt működnek együtt, hogy kovariáns hívásokat hozzanak létre egymással összhangban. A visit a_objectjének van egy encounter_agentje, amit a sensor_data_agent szenzor adataival hívnak meg, melyek forrása az al_universe_object. Egy másik érdekes rész a SPACE_OBJECT osztály, és annak encounter feature-je.

Látogató akció

A SPACE_OBJECT példányok csak a következőket exportálják: encounter (találkozás) ágens, sensor data (szenzoradatok) ágens, és új hely beállítása. Egy űrhajó útja során objektumokat látogat meg az űrben, a szenzoradatok összegyűlnek és átadódnak a látogató objektumnak annak encounter ágensében. Így a sensor_data_agent által közvetített adatok az aktuális objektummal kapcsolatban értékelődnek ki. Ez alapján történnek meg az akciók. A többi adat privát, vagyis az export célja {NONE}, csak az adott objektumon belül használható. Az Eiffel nyelv sajátsága, hogy különböző, de ugyanolyan osztályú objektumok nem hívhatják egymás privát metódusait. Jegyezzük meg, hogy az encounter által meghívott print nem tartalmaz információt arról, hogy milyen alosztályai vannak a SPACE_OBJECT osztálynak! Itt csak az általános SPACE_OBJECT elemeit használjuk. Az a tény, hogy a kimenet értelmes, logikai tervezésen és azon alapul, amit az űrbéli objektumokról tudunk. Maga a SPACE_OBJECT nem tud semmit utódairól.

deferred classSPACE_OBJECTfeature {NONE} -- Initializationmake_with_name (a_name: like name; a_position: like position)    -- Initialize Current with `a_name' and `a_position'.  do    name := a_name    position := a_position    sensor_data_agent := agent sensor_data    encounter_agent := agent encounter  ensure    name_set: name.same_string (a_name)    position_set: position.same_string (a_position)  endfeature -- Accessencounter_agent: PROCEDURE [ANY, TUPLE]    -- Agent for managing encounters with Current.sensor_data_agent: FUNCTION [ANY, TUPLE, attached like sensor_data_anchor]    -- Agent for returning sensor data of Current.feature -- Settingsset_position (a_position: like position)    -- Set `position' with `a_position'.  do    print (type + " " + name + " changes position from " + position + " to " + a_position + ".%N")    position := a_position  ensure    position_set: position.same_string (a_position)  endfeature {NONE} -- Implementationencounter (a_sensor_agent: FUNCTION [ANY, TUPLE, attached like sensor_data_anchor])    -- Detect and report on collision status of Current with `a_radar_agent'.  do    a_sensor_agent.call ([Void])    check attached {like sensor_data_anchor} a_sensor_agent.last_result as al_sensor_data then      if not name.same_string (al_sensor_data.name) then        if (position.same_string (al_sensor_data.position)) then          if ((al_sensor_data.is_dockable and is_dockable) and              (is_manned and al_sensor_data.is_manned) and              (is_manueverable and al_sensor_data.is_not_manueverable)) then            print (type + " " + name + " is near " + al_sensor_data.type + " " +                al_sensor_data.name + " and is dockable.%N")          elseif ((is_dockable and al_sensor_data.is_dockable) and                (is_manned and al_sensor_data.is_manned) and                (is_manueverable and al_sensor_data.is_manueverable)) then            print (type + " " + name + " beams a science team to " + al_sensor_data.type + " " +                al_sensor_data.name + " as they pass!%N")          elseif (is_manned and al_sensor_data.is_not_manned) then            print (type + " " + name + " takes evasive action, avoiding " +                al_sensor_data.type + " `" + al_sensor_data.name + "'!%N")          end        end      end    end  endname: STRING    -- Name of Current.type: STRING    -- Type of Current.  deferred  endposition: STRING    -- Position of Current.is_dockable: BOOLEAN    -- Is Current dockable with another manned object?  deferred  endis_manned: BOOLEAN    -- Is Current a manned object?  deferred  endis_manueverable: BOOLEAN    -- Is Current capable of being moved?  deferred  endsensor_data: attached like sensor_data_anchor    -- Sensor data of Current.  do      Result := [name, type, position, is_dockable, not is_dockable, is_manned, not is_manned, is_manueverable, not is_manueverable]    end  sensor_data_anchor: detachable TUPLE [name, type, position: STRING; is_dockable, is_not_dockable, is_manned, is_not_manned, is_manueverable, is_not_manueverable: BOOLEAN]      -- Sensor data type anchor of Current.end

Jelenleg három leszármazottja van a SPACE_OBJECT osztálynak:

SPACE_OBJECTASTEROIDSPACESHIPSPACESTATION

Példánkban az ASTEROID aszteroidát, a SPACESHIP űrhajót, és a SPACESTATION a Deep Space Nine űrállomást jelenti. Mindegyik bizonyos tulajdonságokat és a type feature-t állítja be. A name a konstruktor (creation) és a position számára szükséges.

classSPACESHIPinheritSPACE_OBJECTcreatemake_with_namefeature {NONE} -- Implementationtype: STRING = "Starship"  -- <Precursor>is_dockable: BOOLEAN = True  -- <Precursor>is_manned: BOOLEAN = True  -- <Precursor>is_manueverable: BOOLEAN = True  -- <Precursor>end

Tehát egy SPACESHIP objektumnak van személyzete, manőverezhető és dokkolható. Ezzel szemben egy aszteroidának nincsenek ilyen tulajdonságai. Egy SPACESTATION objektumnak van személyzete és dokkolható, de nem manőverezhető. Az egymás közelében levő objektumok tulajdonságaik szerint kapcsolatba lépnek egymással. A név azonosítja a példányokat, tehát az ugyanolyan nevű és típusú példányok kölcsönhatását a rendszer megakadályozza.

Eiffel összefoglaló

Az Eiffel ágensei lehetővé teszik, hogy egy lépéssel jobban el lehessen távolítani az objektumokat egymástól. A rutinokat az osztályból kiemelve ágenssé alakítjuk, és ahelyett, hogy közvetlenül az objektumokat hívogatnánk. Az ágensek bírnak szignatúrával és lehet, hogy visszatérési értékkel is, amivel ideális alanyai a statikus típusellenőrzésnek, anélkül, hogy felfednék az objektumok specifikus részleteit. Ugyanakkor teljes joggal polimorfak, így csak az eredmény kód bír azzal a tudással, ami a munka elvégzéséhez szükséges. Másrészt az objektumok részleteinek elrejtése a többi kovariáns objektum elől a karbantartást is támogatja. Mindezt biztosítják az ágensek. Velük bonyolultabb helyzetek is megoldhatók, lásd multimetódus.

Az ágensek használatának hátránya, hogy lassabban hívhatók, mint a normál tagfüggvények; úgyhogy ha valamit el lehet végezni azokkal is, akkor nem kell az ágenseket bevetni. Ha azonban lényegi változások várhatók a felhasznált osztályokban, akkor mégis szükség van ágensekre a karbantarthatóság érdekében.

Jegyzetek

Fordítás

Ez a szócikk részben vagy egészben a Double dispatch című angol Wikipédia-szócikk 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.