A Lua programozási nyelv

Metatáblák és metafüggvények



Objektumok

A Lua -nyelvi elemeit tekintve- nem támogatja az objektum-orientált fejlesztést. Azonban a nyelv beépített tábla típusa felfogható egy objektumként. Akárcsak az objektumok a táblák is rendelkeznek egy azonosítóval (általában self-nek nevezzük de más nevet is használhatunk) ami független a tábla tartalmától, két tábla azonos értékekkel különböző objektumnak számít. Továbbá a táblák is rendelkeznek élettartammal, ami független attól, hogy hol vagy ki által lettek létrehozva. Akár csak az objektumok a Lua tábla típusa is rendelkezhet saját műveletekkel

Szamla = {egyenleg = 0} function Szamla.penzKivetel (v) Szamla.egyenleg = Szamla.egyenleg - v end

A penzKivetel valójában a Szamla tábla egy mezőneve ami egy függvényt tartalmaz. Amit a követketőképpen hívhatunk meg:

Szamla.penzKivetel(100)

Az egyel fentebb látható kódrészlet penzKivetel mezőjében tárolt függvény, már majdnem megegyezik egy tagfüggvénnyel, azonban a függvényünk még egy globális változót a Szamla-t használja a törzsében. Ennek következtében egyrészt a függvény csak azon az objektumon fog működni ahol definiáltuk, másrészt a ha a Szamla globális változóban tárolt objektum megszűnik, mert kinullázuk, vagy átállítjuk máshova, vagy új változóban tároljuk az objektumot a függvényünk máris használhatatlanná válik:

s = Szamla ; Szamla = nil s.penzKivetel(100) --Hiba!!!!

A fentebb leírtak ellentmondásba kerülnek azzal az állítással, hogy az objektumok egymástól függetlenül léteznek. Ahhoz, hogy egy függvény tagfüggvény szerűen viselkedjen át kell adnunk neki egy még egy paramétert, az objektumot amin a meghívtuk azt.

function Szamla.penzKivetel (self, v) self.egyenleg = self.egyenleg - v end

A paraméter neve tetszőleges, de a self és a this az elterjedt elnevezés. Ha a függvényünket az alábbiak szerint módosítjuk, akkor már biztonságosan használhatjuk azt, nem függünk egy külső globális változótól. A fentebb leírt metódus a következőképpen hívható meg:

a1 = Szamla a1.penzKivetel(a1, 100.00) --explicit átadva a1:penzKivetel(100.00) --implicit átadva

Ha nem akarjuk a hívó objektumot explicit módon átadni akkor a : operátor segítségével hívva a függvényt automatikusan átadásra kerül.

Osztályok

Az osztályok (class) valójában az objektumok létrehozásának egy speciális formája. Sok objektum orientált nyelv rendelkezik az osztályok koncepciójával. Az ilyen nyelvekben az objektumok osztály példányok. A Lua nem rendelkezik osztály fogalommal, minden objektum magénak definiálja a működését és az alakját. Ennek ellenére lehetséges az osztályokhoz hasonló viselkedés előidézése a Lua-ban (nem minden tudunk azonban megoldani, lásd később virtuális függvények)

A Lua megoldása az osztályokra nagyon hasonlít a prototípus alapú nyelvekéhez (Self, NewtonScript). Az ilyen nyelvekben az objektumok rendelkezhetnek egy prototípussal, ami egy objektum, ahonnan az egyes objektumok a hiányzó műveleteket megtalálják. Ahhoz, hogy ehhez hasonló nyelvekben írjunk egy osztályt, egyszerűen létrehozunk egy objektumot amit a későbbi példányaink felhasználnak.

Ahhoz, hogy ezt megtegyük szükségünk van a metatábla (metatable) __index tagjának használatára. Ha a Lua-ba megpróbáluk elérni egy olyan táblamezőt ami nem létezik például megpróbálnánk elérni a Szamla tábla tulajdonos mezőjét, akkor az eredmény nil lesz, különben a metatábla segítségével meghatározzuk az értéket.

Szamla.tulajdonos --nil az érték Szamla.egyenleg -- ezt meg tudjuk határozni

Valójában az történik, hogy megnézzük van-e a táblánknak adott nevű mezője és ha nincs, akkor a metatáblájában az __index tagnak nézzük meg, hogy van-e ilyen tagja. A lenti példakód szemlélteti, hogy hozzunk létre metatáblát és egy "konstruktort".

Szamla = { egyenleg } -Szamla tábla az egyenleg nevű mezővel Szamla_mt = {__index = Szamla} --Szamla metatábla function Szamla.letrehozas(egyenleg_) --Az ugynevezett konstruktor függvény local inst = {} -- az új objektumunk setmetatable(inst,Szamla) -- beállítjuk az metatáblába inst.egyenleg = egyenleg_ -- inicializáljuk az új objektumot return inst end --gyorsabb verzió: function Szamla.letrehozas(egyenleg_) local inst = {egyenleg=egyenleg_} -- az új objektumunk setmetatable(inst,Szamla) -- beállítjuk az metatáblába return inst end function Szamla:penzKivetel(amount) self.egyenleg = self.egyenleg - amount end -- objektum létrehozása és használata acc = Szamla.letrehozas(1000) acc:penzKivetel(100)

A metatábla beállítása sor a kulcs, ez a sor gondoskodik arról, hogy a műveleteket és az adattagokat megtaláljuk a prototípusban. Ennek következményeként, ha a prototípus objektumban beállítunk egy mezőnek egy értéket és az újban még nem tettük ezt meg, akkor a prototípus értékét kapjuk vissza ha először használjuk azt a mező.

Öröklés

Mivel az osztályok valójában objektumok, ezért kaphatnak műveleteket más osztályoktól is, ez a viselkedés használható, mint öröklés. Az örökléshez először rendelkeznünk kell egy ősosztállyal, használjuk erre a célra a fentebb létrehozott Szamla osztályt. Hozzuk most létre egy olyan számlát aminek a penzKivetel nem egyszerűen levonja az összeget, hanem megengedi, hogy mínuszba menjünk egy bizonyos határig.

LimitesSzamla = Szamla:new() -- a speciális számla az eredetiből származik s = LimitesSzamla:new{limit=1000.00} --létrehozunk egy ilyen számlát 1000 limittel function LimitesSzamla:penzKivetel (v) if v - self.egyenleg >= self:getLimit() then error" Nem leheted at a limited!!" end self.egyenleg = self.egyenleg - v end function LimitesSzamla:getLimit () return self.limit or 0 end

A fentebb látható példa szemlélteti az öröklés menetét. Az ősosztály függvényit felül tudjuk definiálni, hogy a leszármazottban másképp viselkedjenek.

Polimorfizmus, Virtuális függvények

Ahhoz, hogy ezt a viselkedést szimuláljuk rengeteg plusz kód megírására van szükségünk, kezve az alapoktól: Osztályok létrehozása, konstruktorok, virtuális tábla az objektumokhoz. Rengetegfajta osztály implementáció létezik jelenleg a Lua-hoz ami vagy támogatja ezt a viselkedést, vagy csak az elfedést alkalmazza. Az iménti oldalon: https://github.com/jpatte/yaci.lua található egy "könyvtár", ami folyamatosan fejlődik és jelenleg a legtöbbet tudja. A könyvtárról a http://lua-users.org/wiki/YetAnotherClassImplementation oldalon is található egy leírás példákkal. A "könyvtár" használatára egy példa ez a program. A feladat egy másik megvalósítása ami saját osztály implementációt tartalmaz és csak az elfedést teszi lehetővé megtalálható itt.

Többszörös Öröklés

Mivel az objektumok elég összetettek a Lua-ban, ezért többféleképpen is tudunk objektum orientált viselkedést szimulálni. A fentebb leírtak, a metatábla használatáról valószínűleg a legegyszerűbb, leghatékonyabb és rugalmasabb megoldás. Azonban vannak esetek, amikor egy másik megoldás alkalmasabb a céljaink elérésére. A többszörös öröklés eléréséhez egy másik ábrázolásra van szükségünk. Ennek az implementációnak a kulcsa a metatáblabeli __index használata egy speciális függvényen keresztül. A Lua ha nem talál egy kulcsot (a táblák asszociatív tömbök) az adott objektumban akkor elkezdi keresni a metatáblában, ahol az __index-en keresztül tetszőleges számú ősben kerestethetjük azt.

Szemely = {} function Szemely:getname () return self.name end function Szemely:setname (n) self.name = n end -- k keresése a SzulokLista-ben felsorolt táblákban local function search (k, SzulokLista) for i=1, #SzulokLista do local v = SzulokLista[i][k] -- az i. ős átvizsgálása if v then return v end end end function peldanyositas (...) local c = {} -- új osztály local szulok = {...} -- az osztály minden metódusát az ősökben keresi majd setmetatable(c, {__index = function (t, k) return search(k, szulok) end}) -- c metatábla beállítása c.__index = c -- konstruktor létrehozása c hez function c:new (o) o = o or {} setmetatable(o, c) return o end return c -- a kész c visszaadása end

Információ elrejtése

A Lua filozófia azt mondja, hogy ha nem tanácsos valamit megváltoztatni, hát ne tegyük. Tehát ha azt akarjuk, hogy bizonyos adattagok kívülről ne legyenek láthatóak, akkor trükköt kell bevetnünk, mert bizony a nyelv ezt nem biztosítja számunkra.

Az ötlet a következő: a példányosítás során egy külön tömbben tároljuk az adatokat és egy másikban az eljárásokat (interface). A metódusok hívása a második tömbből lehetséges, amely nem tartalmazza az elsőt. Ennek szemléltetésére nézzük a következő kódrészletet:

function szamlaLetrehozas (kezdoEgyenleg) local self = {egyenleg = kezdoEgyenleg} local penzKivetel = function (v) self.egyenleg = self.egyenleg - v end local penzBefuzetes = function (v) self.egyenleg = self.egyenleg + v end local getegyenleg = function () return self.egyenleg end return { penzKivetel = penzKivetel, penzBefuzetes = penzBefuzetes, getegyenleg = getegyenleg } end

Az implementáció kulcsa az, hogy az egyes függvények hívásakor nem kerül átadásra a self hanem, közvetlenül érjük azt el.

Egy metódusos osztályok

Előfordulhat, hogy olyan objektumot szeretnénk létrehozni, aminek csak egyetlen egy metódusa van például funktorok. Másik érdekes lehetőség egy olyan objektum aminek egyetlen metódusa egy kapcsoló szerű függvény, ami más műveletet hajt végre a paraméterének függvényében. Egy lehetséges implementáció erre a következő függvény:

function FunktorSzeru (value) return function (action, v) --C# szerű get-er , set-er if action == "get" then return value elseif action == "set" then value = v else error("hibás akció") end end end --használata d = FunktorSzeru(0) print(d("get")) -- 0 d értéke d("set", 10) print(d("get")) -- 10 d értéke