A számítástudományban a dinamikus küldés az a folyamat, amelynek során kiválasztjuk, hogy egy polimorf művelet (metódus vagy függvény) melyik implementációját hívjuk meg futásidőben. Általában az objektumorientált programozási (OOP) nyelvekben és rendszerekben alkalmazzák, ezek elsődleges jellemzőjének tekintik.

Az objektumorientált rendszerek a problémát egymással kölcsönhatásban álló objektumok halmazaként modellezik, amelyek a névvel hivatkozott műveleteket hajtják végre. A polimorfizmus az a jelenség, amikor a részben felcserélhető objektumok mindegyike azonos nevű, de esetleg eltérő viselkedésű műveletet hajt végre. Például egy fájlobjektum és egy adatbázis-objektum egyaránt rendelkezik egy StoreRecord metódussal, amely egy személyzeti rekord tárolóba írására használható, a megvalósításuk különbözik. A program egy objektumra mutató hivatkozást tart, amely lehet fájl- vagy adatbázis-objektum. Azt, hogy melyik, egy futásidejű beállítás határozhatja meg, és ebben a szakaszban a program nem tudhatja, vagy nem is érdekli, hogy melyik. Amikor a program meghívja a StoreRecordot az objektumon, valaminek ki kell választania, hogy melyik viselkedés lépjen életbe. Ha az OOP-ra úgy gondolunk, mint üzenetek küldésére objektumoknak, akkor ebben a példában a program StoreRecord üzenetet küld egy ismeretlen típusú objektumnak, és a futásidejű támogató rendszerre bízza az üzenet megfelelő objektumnak való továbbítását. Az objektum azt a viselkedést hajtja végre, amelyet megvalósít.[1]

A dinamikus küldés ellentétben áll a statikus küldéssel, amelyben a polimorf művelet megvalósítását a fordítás idején választják meg. A dinamikus küldés célja, hogy a megfelelő implementáció kiválasztását addig halassza, amíg egy paraméter (vagy több paraméter) futáskori típusa nem ismert.

A dinamikus küldés különbözik a késői kötéstől (más néven dinamikus kötéstől) is. A névkötés egy nevet társít egy művelethez. Egy polimorf műveletnek több megvalósítása van, amelyek mindegyike ugyanahhoz a névhez kapcsolódik. A kötés történhet fordítási időben vagy (késői kötés esetén) futási időben. A dinamikus küldéssel egy művelet egy adott megvalósítását választjuk ki futáskor. Míg a dinamikus küldés nem feltételez késői kötést, a késői kötés dinamikus küldést feltételez, mivel a késői kötésű művelet végrehajtása csak futási időben ismert.

Egyszeri és többszöri küldés

szerkesztés

A választás, hogy egy metódus melyik változatát hívjuk meg, alapulhat egyetlen objektumon vagy objektumok kombinációján. Az előbbit nevezzük egyszeri küldésnek, és közvetlenül támogatják az olyan elterjedt objektumorientált nyelvek, mint a Smalltalk, a C++, a Java, a C#, az Objective-C, a Swift, a JavaScript és a Python. Ezekben és a hasonló nyelvekben meg lehet hívni egy metódust az osztáshoz alábbihoz hasonló szintaxissal:

osztalék.osztás(osztó) # osztalék / osztó

ahol a paraméterek opcionálisak. Ezt úgy kell elképzelni, mint egy osztás nevű üzenet küldését osztó paraméterrel az osztalékhoz. A végrehajtás csak az osztó típusa alapján lesz kiválasztva (esetleg racionális, lebegőpontos, mátrix), figyelmen kívül hagyva az osztó típusát vagy értékét.

Ezzel szemben egyes nyelvek az operandusok kombinációja alapján küldik el a metódusokat vagy függvényeket; az osztás esetében az osztó és az osztó típusa együttesen határozza meg, hogy melyik osztási művelet kerül végrehajtásra. Ezt nevezzük többszörös küldésnek. Példák a többszörös küldést támogató nyelvekre a Common Lisp, a Dylan és a Julia.

Dinamikusküldés-mechanizmusok

szerkesztés

Egy nyelv különböző dinamikusküldés-mechanizmusokkal valósítható meg. A nyelv által kínált dinamikusküldés-mechanizmus választása nagymértékben megváltoztatja az adott nyelven belül elérhető vagy legtermészetesebben használható programozási paradigmákat.

Normális esetben egy típusos nyelvben az argumentumok típusa alapján (leggyakrabban az üzenet címzettjének típusa alapján) történik az elküldési mechanizmus. A gyengén vagy nem típusos nyelvek gyakran hordoznak egy küldéstáblázatot az objektumadatok részeként minden objektumhoz. Ez lehetővé teszi a példányviselkedést, mivel minden objektumpéldány külön metódushoz rendelhet egy adott üzenetet.

Egyes nyelvek hibrid megközelítést kínálnak.

A dinamikus küldés mindig költségekkel jár, ezért egyes nyelvek bizonyos metódusokhoz statikus elküldést kínálnak.

C++-megvalósítás

szerkesztés

A C++ korai kötést használ, és egyaránt kínál dinamikus és statikus küldést. A küldés alapértelmezett formája statikus. A dinamikus küldéshez a programozónak virtuálisnak (virtual) kell jelölnie a metódust.

A C++-fordítók jellemzően egy virtuális függvénytáblának (vtable) nevezett adatszerkezettel valósítják meg a dinamikus küldéseket, amely egy adott osztály név-implementáció leképezését tagfüggvénymutatók halmazaként határozza meg. (Ez pusztán egy implementációs részlet; a C++ specifikáció nem említi a vtable-öket). Az adott típus példányai ezután a példányadataik részeként tárolnak egy mutatót erre a táblára. Ez bonyolult, ha többszörös öröklést használunk. Mivel a C++ nem támogatja a késői kötést, a C++ objektum virtuális táblázata nem módosítható futásidőben, ami az elküldési célpontok lehetséges halmazát a fordításkor kiválasztott véges halmazra korlátozza.

C++-ban a túlterhelés nem eredményez dinamikus küldéseket, mivel a nyelv az üzenet paramétereinek típusait a formális üzenetnév részének tekinti. Ez azt jelenti, hogy a programozó által látott üzenetnév nem a kötéshez használt formális név.

Go- és Rust-megvalósítás

szerkesztés

Góban és Rustban a korai kötés egy sokoldalúbb változata használatos. A vtable-mutatókat objektumhivatkozásokkal „kövér mutatókként” (Góban „interfészek”, Rustban pedig „vonás-objektumok”) hordozzák.

Ez elválasztja a támogatott interfészeket a mögöttes adatstruktúráktól. Minden lefordított könyvtárnak nem kell ismernie a támogatott interfészek teljes skáláját ahhoz, hogy helyesen használhasson egy típust, csak az adott vtable-elrendezést, amit igényel. A kód különböző interfészeket adhat át ugyanazon adathoz különböző függvényeknek. Ez a sokoldalúság az egyes objektumhivatkozásokkal járó extra adatok rovására megy, ami problémás, ha sok ilyen hivatkozást tartósan tárolnak.

Smalltalk-megvalósítás

szerkesztés

A Smalltalk típusalapú üzenetküldő rendszert használ. Minden példánynak egyetlen típusa van, amelynek definíciója tartalmazza a metódusokat. Amikor egy példány üzenetet kap, a diszpécser megkeresi a megfelelő metódust a típushoz tartozó üzenet-módszer térképen, majd meghívja a metódust.

Mivel egy típusnak több alaptípusa is lehet, ez a keresés költséges lehet. A Smalltalk mechanizmusának naiv megvalósítása lényegesen nagyobb többletköltséggel járna, mint a C++-ban, és ez a többletköltség minden egyes üzenetnél jelentkezne, amelyet egy objektum kap.

A valódi Smalltalk-implementációk gyakran használnak egy inline caching[2] néven ismert technikát, amely nagyon gyorssá teszi a metóduskiadást. Az inline caching alapvetően az előző célmetódus címét és a hívás helyének objektumosztályát tárolja (vagy több párt a többirányú gyorsítótárazás esetén). A gyorsítótárban tárolt metódus a metódusválasztó alapján a leggyakoribb célmetódussal (vagy csak a cache miss kezelővel) inicializálódik. Amikor a végrehajtás során a metódushívó helyet elérjük, az csak a gyorsítótárban lévő címet hívja meg. (Egy dinamikus kódgenerátorban ez a hívás közvetlen hívásnak minősül, mivel a közvetlen címet a cache miss logika visszapatcheli.) A meghívott metódusban lévő prológus kód ezután összehasonlítja a gyorsítótárban lévő osztályt a tényleges objektum osztályával, és ha nem egyeznek, a végrehajtás elágazik egy cache miss kezelőhöz, hogy megtalálja a megfelelő metódust az osztályban. Egy gyors implementáció több gyorsítótár-bejegyzéssel is rendelkezhet, és gyakran csak néhány utasításra van szükség ahhoz, hogy a végrehajtás a megfelelő metódushoz vezessen egy kezdeti gyorsítótár-kihagyás esetén. A leggyakoribb eset a gyorsítótárban lévő osztály egyezése lesz, és a végrehajtás egyszerűen a metódusban folytatódik.

A soron kívüli gyorsítótárazást a metódusfelhívási logikában is használhatjuk, az objektumosztály és a metódusválasztó segítségével. Az egyik kialakításban az osztály és a metódusválasztó hasheléssel van ellátva, és egy metóduskiadási gyorsítótábla indexeként használják.

Mivel a Smalltalk egy reflektív nyelv, sok implementáció lehetővé teszi az egyes objektumok mutációját dinamikusan generált metóduskereső táblázatokkal objektumokká. Ez lehetővé teszi az objektumok viselkedésének objektumonkénti módosítását. A prototípus-alapú nyelvek egész kategóriája nőtt ki ebből, a leghíresebbek közülük a Self és a JavaScript. A metóduselosztási gyorsítótár gondos megtervezése még a prototípus-alapú nyelveknél is lehetővé teszi a nagy teljesítményű metóduselosztást.

Számos más dinamikusan típusos nyelv – például a Python, a Ruby, az Objective-C és a Groovy – is hasonló megközelítést alkalmaz.

Példa Pythonban

szerkesztés
class Macska:
    def beszél(önálló):
        print("Miau")

class Kutya:
    def beszél(önálló):
        print("Vau")


def beszél(állat):
    # Dinamikusan meghívja a beszél metódust
    # az állat lehet a Macska vagy a Kutya egy példánya
    állat.beszél()
    
macska = Macska()
beszél(macska)
kutya = Kutya()
beszél(kutya)
  1. (1995) „Message Dispatch on Pipelined Processors”. ECOOP. 
  2. Müller, Martin (1995), Message Dispatch in Dynamically-Typed Object-Oriented Languages, pp. 16–17

Fordítás

szerkesztés
  • Ez a szócikk részben vagy egészben a Dynamic dispatch 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.

Bibliográfia

szerkesztés