A Lua egy dinamikusan típusos nyelv. Ez azt jelenti, hogy a változóknak nincs típusa, viszont az értékeknek van (minden érték hordozza a típusát). A nyelv alapból referencia szerint végez egyenlõség vizsgálatot, kivételt ezalól csak a nil, boolean, string és number típusok képeznek.
A nyelvben minden értékhez tartoznak ún. tag-ek. Ennek segítségével
metódusokat definiálhatunk az egyes típusú értékekhez. Ez ad lehetőséget arra,
hogy meg tudjuk különböztetni, hogy Lua vagy C típusú értéket tárol egy adott
function
típusú változó.
Saját típusok konstrukciójára nincs lehetőség, viszont érdekes lehetőségek rejlenek a táblákban. Ld.: Objektum-orientált programozás.
A stringek Luában tetszőleges, minimum 8 bites értékek sorozatai. Közvetlenül
a C fordító char
típusára képeződnek le (ami lehet 8 bitesnél hosszabb is,
ezért a minimum). A nyelv nem foglal le magának konkrét karakter értékeket, így a \0
-t sem.
Ennek köszönhetően tetszőleges Unicode kódolású szöveget tárolhatunk Lua stringekben.
Az input és output az stdio
C könyvtárat használja, amely szöveges
módban nem garantálja tetszőleges byte szekvenciák megfelelő kezelését, ráadásul bizonyos
byte sorozatok másokra konvertálódnak, pl. a platformspecifikus sorvége jelölés miatt.
A fájlokat ezért érdemes mindig bináris módban megnyitni.
Amíg az Unicode szövegeket csak a kódolást támogató külső könyvtáraknak továbbíttjuk, nem kell problémáktól tartanunk. Az egyenlőségvizsgálat vagy a standard könyvtár nemtriviális string műveleteinek használata (pl. mintaillesztés) viszont ilyen stringek esetén nem ajánlott.
A Lua automatikus futási idejű konverziót hajt végre számok és stringek között. Minden aritmetikai művelet a string típusú paraméteréből megpróbál számot készíteni a szokásos szabályok szerint, ahol pedig stringre lenne szükség, ott a számok alakulnak át valamilyen értelmes formájú stringgé. A számok megjelenési formáját meg is lehet adni a standard könyvtár segítségével.
A Lua egyetlen összetett típusa és egyúttal típuskonstrukciós eszköze
a table
típus.
A táblák segítségével szimulálhatjuk a hagyományos tömb és a
rekordszerkezet típusait, hiszen az asszociatív tömb olyan leképezést ír le,
amely tetszőleges (nem nil
) típusú értékhez egy másik (szintén
tetszőleges típusú) elemet rendel.
A fenti összetett típusok típusműveleteit implementálhatjuk függvények segítségével, melyeket a táblában tárolhatunk.
A táblák működésének egy másik érdekes jellemzője csak a Lua 5.0 óta szerepel a nyelvben. A 4.0-s verzióval bezárólag a táblák szigorúan hash táblaként voltak implementálva: minden párt explicit tároltak. Az újabb verziókban optimalizálási célokból az egész kulcsú párok kulcsa nem tárolódik, az értékek pedig egy valódi tömbbe kerülnek. Pontosabban, a táblák hibrid adatszerkezetek: tartalmaznak egy hash tábla részt és egy tömb részt. Mivel a felosztás alacsony szinten történik, a tábla mezők hozzáférése egységes, még a virtuális gép számára sincs különbség a kétféleképpen tárolt értékek közt.
A táblák automatikusan és dinamikusan igazítják a részeiket a tartalmukhoz: a tömb rész megpróbálja az 1-től valamely n határig terjedő egész indexek értékeit tárolni. A nem egész indexek, illetve az intervallumon kívül eső egész indexek a hast tábla részbe kerülnek. Ha egy tábla mérete nő (új elemet teszünk bele), a Lua újraszámolja a hash tábla és a tömb méretét. A tömb rész mérete az a legnagyobb n, amire az 1 és n közti indexek legalább fele tartalmaz értéket (hogy ne pazaroljuk a helyet) és legalább egy felhasznált index van n/2 + 1 és n közt (hogy ne legyenek feleslegesen dupla méretű tömbjeink). A tömb és a hash tábla is lehet végül üres. Az új méretek kiszámítása után a Lua létrehozza az új részeket, és újra beszúrja az elemeket a megfelelő helyre.
Egy példa:
Legyen a
egy üres tábla, amelynek mind a tömb, mind a hash tábla részének
mérete 0. Az a[1] = v1
utasítás végrehajtásához a tábla méretét növelni kell.
A Lua az n = 1 -et választja a tömb új méreteként, egyetlen elemmel, a hash tábla rész
pedig üres marad. Ha a következő utasítás a[5] = v2
, a méretek újraszámításakor
a tömb nem nőhet 5 hosszúra, hiszen 1 és 5 közt csak két kihasznált index lenne.
A tömb mérete így 1 marad, a hash tábla pedig szintén 1-re nő, és az 5
-ös kulcshoz
a v2
értékét tárolja. Ha ezután pl. az a[2] = v3
utasítás következik,
a méretek kiszámítása után mindhárom érték az 5 hosszú tömbbe kerül, a hash tábla pedig megint
üres lesz.
A hibrid sémának két előnye van. Egyrészt az egész kulcsú értékekhez való hozzáférés gyorsabb, mivel nincs szükség hashelésre. Másrészt, ami még fontosabb, a tömb rész nagyjából fele akkora memóriát foglal, mintha ugyanezt a hash táblában tárolnánk, mivel a kulcsok csak implicit léteznek. Következésképpen egy megfelelően sűrűn indexelt tábla tömbként használva tömbökre jellemző teljesítményt nyújt. Mivel a hash tábla rész üres, se sebességben se memóriaigényben nem befolyásolja negatívan a tömböt. Ugyanakkor, ha asszociatív tömbként használjuk a táblát, a tömbrész jó eséllyel üres, így nem foglal plusz memóriát. Ezek a memória megtakarítások fontosak, hiszen Luában gyakran használunk sok kis táblát, például objektumok implementációjára.
Az egyszerű változóhivatkozások a legtöbb nyelvhez hasonlóan a változó nevének leírásával történnek. Táblák elemeinek elérésére a tömböknél elterjedt szintaxist lehet használni:
Gyakran használt eset, amikor string típusú a kulcs - például rekordokat lehet így készíteni. Ezt a felhasználást segítendő, a következő két sor ekvivalens:
A Lua háromféle változót ismer: globális és lokális változót, valamint
táblamezőt. A függvények formális paraméterei például speciális lokális
változók. Lokális változók deklarációja a nevük elé írt local
kulcsszóval történik.
A nyelv lexikális láthatóságot használ. Egy változó hatóköre a deklarációt követő első utasításnál kezdődik, és az azt tartalmazó legszűkebb blokk végéig tart. Példa a láthatóságra:
Ha egy változót nem explicit módon lokálisként deklarálunk, akkor
globálisnak számít (minden globális változó "létezik"). A lokális változókhoz
a hatókörükön belül definiált függvények szabadon hozzájuk férhetnek. Az első
értékadás előtt egy változó értéke mindig
nil
.
Minden globális változó hagyományos Lua táblákban, ún. környezetekben tárolódik.
A változó nevéhez mint kulcshoz van hozzárendelve annak értéke. A Luába exportált C
függvények mind egy közös környezeten osztoznak. Minden Luában írt függvénynek saját
referenciája van egy környezetre, így a függvényen belül minden globális változó erre a
táblára hivatkozik. Egy függvény létrehozásakor örökli a létrehozójának környezetét. A
környezetek lekérdezésére és megváltozatására a getfenv
és setfenv
függvények állnak rendelkezésre.
A nyelvben a következő elemi kifejezésekből lehet operátorok segítségével kifejezéseket építeni:
Alapkifejezésnek számít még a zárójelezés, amely a szokásos jelentése mellett azt is megteszi, hogy ha a belső kifejezés több értéket ad eredményül, akkor csak az elsőt hagyja meg, a többit eldobja.
A változóhivatkozások leírása a Változók, a függvénydefiníciók az Alprogramok pontban vannak leírva.
Egy függvényt Luában is az általánosan elterjedt szintaxissal hívhatunk meg:
Paraméter nélküli függvény meghívásakor ki kell tenni az üres zárójelpárt.
Ezen kívül néhány rövidítést lehet használni egy paraméteres függvényeknél:
Az első három sor string literál, az utolsó táblakonstrukció átadását teszi lehetővé egyetlen paraméterként; ugyanazt jelentik, mintha az az egy paraméter zárójelbe lenne téve.
Az utolsó rövidítés az objektum orientált programozást támogatja. Objektumok
példánymetódusainak hívásakor a metódus kap egy "rejtett" paramétert, a
this
hivatkozást (itt self
). Luában ezt az implicit paramétert ki kellene írni,
de erre is bevezettek egy rövidítést, így az alábbi két sor ugyanazt jelenti:
A táblát asszociatív tömb formájában implementáljuk. Az asszociatív tömb indexmezeje nem csak szám, hanem bármilyen
típus lehet, ami a Luában fellelhető kivéve a Nil-t. Sőt, a tábláknak nem kell fix méretet megadni, akármekkora lehet - illetve
amekkorát a memória megenged. A tábla a fő - valójában az egyetlen - adatszerkezet a Lua nyelvben. Táblákkal reprezentáljuk a
"hagyományos" tömböket, szimbolikus táblákat, halmazokat, sorokat, rekordokat és más adatszerkezeteket. Gyakorlatilag a csomagkezelés
is táblákkal történik. Mikor azt mondjuk, hogy io.read
, ezzel azt szeretnénk elérni, hogy használjuk a io
csomag
read
metódusát. De a háttérben az io
tábla read
mezőjét - indexét - hívjuk meg.
A legegyszerűbb táblakonstrukció az alábbi:
nil
lesz.
Táblákat létre lehet hozni úgy, hogy egyesével minden kulcshoz értékadással hozzárendeljük a megfelelő értéket. Ennek alternatívájaként olyan kifejezéseket is lehet írni, amelyek táblát adnak eredményül - mintha "tábla literált" írnánk.
A táblakonstrukciós kifejezések kapcsos zárójelek közé zárt mezőfelsorolások. A mezők kulcs-érték párjait vesszővel vagy pontosvesszővel elválasztva kell felsorolni (a két elválasztójelet akár felváltva is használhatjuk), a párokat pedig többféleképpen is meg lehet adni:
Az első a legáltalánosabb alak, a szögletes zárójelek közé bármilyen típusú kifejezést be lehet írni. A második és a harmadik pár megegyezik, ez a szokásos rövidítés a string típusú kulcsokra a rekordok leírásának támogatására. Az utolsó esetben nincs megadva kulcs; ez a táblák leírását kényelmesebbé tevő lehetőség. Az egész konstrukció kap egy 1-től induló számlálót, és minden kulcs nélküli érték ennek a számlálónak az értékét kapja kulcsként, valamint minden ilyen elem elhelyezése után növekszik eggyel a számláló. A másik két típusba tartozó kulcs-érték párok nem számítanak bele ebbe a számolásba.
Ha az utolsó, kulcs nélküli értéket egy függvényhívás szolgáltatja, és ez a függvény több értéket is visszaad, az megint speciális jelentéssel bír: ekkor az összes visszaadott érték bekerül egymás után a táblába. Ezt zárójelezéssel lehet meggátolni, amely csak az első visszaadott értéket engedi betenni a táblába.
A lista végén álló elválasztójelet a Lua figyelmen kívül hagyja. Ezt a kényelmi funkciót a géppel generált kód támogatására vezették be.
Íme egy kimerítő példa:
Mint említettük, a rekordokat is táblákkal valósítjuk meg:
Táblák segítségével listák konstruálása is egyszerű, például:
A nyelvben a következő operátorok használhatóak, precedencia szerint csökkenő sorrendben:
Operátor | Jelentés | Megjegyzés |
---|---|---|
^ | hatványozás | jobbasszociatív |
-x, not, # | aritmetikai, logikai negáció | |
*, / | szorzás, osztás | |
+, - | összeadás, kivonás | |
.. | string konkatenáció | jobbasszociatív |
==, ~=, <, >, <=, >= | relációs műveletek | |
and | logikai és | rövidzáras szemantika szerint működnek |
or | logikai vagy |
Az egyenlőség vizsgálata nil, a logikai értékek, a számok és a stringek esetén
érték szerint történik.
Minden más típust referencián keresztül lehet elérni, és az egyenlőségvizsgálat
ennek a referenciának az egyenlőségét vizsgálja csak. Ha a két operandus típusa
nem egyezik meg, akkor hamisat ad eredményül; még a számok és stringek közötti
automatikus konverzió sem működik itt.
Így tehát a "0"==0 kifejezés értéke false
, ugyanígy
a t[0]
és t["0"]
a tábla különböző elemeit jelentik.
A ~= operátor értéke az egyenlőségvizsgálat (==) negáltja.
A Lua nyelv biztosít egy hossz prefix operátort is, amit #-tel jelölünk. Egy string hossza a string bájtjainak száma (#„helló” == 6).
Táblának csak akkor definiáljuk a hosszát, ha az egy sorozat, vagyis ha az indexei az {1..n} halmaz elemei. Ebben az esetben a tábla hossza n, különben nem definiált.
A Lua logikai operátorai: and
, or
, not
.
Ezek az operátorok a false
és nil
értékeket tekintik hamisnak és
bármi mást (beleértve a nullát és az üres stringet) igaznak.
Ennek főleg történelmi okai vannak, a boolean
típus
ugyanis elég későn jelent meg a nyelvben.
A not
mindig false
vagy true
értéket ad vissza.
Az and
az első argumentumát adja vissza, ha az false
vagy nil
,
egyébként a másodikat.
Az or
az első argumentumát adja vissza, ha az nem false
vagy nil
,
egyébként a másodikat.
Példák:
A logikai operátorokkal lehetőségünk van függvényekben default argumentumok használatára.
Erre az or
operátor szemantikája és a függvényparaméterek szabad kezelése ad
lehetőséget, ahogy az alábbi példán láthatjuk:
A Luában bármely függvényt meghívhatjuk (többek közt) úgy, hogy nem minden argumentumot
adunk meg. Ha egy, az argumentumlistában szereplő paramétert nem tüntettünk fel a híváskor,
értéke a függvény törzsében nil
lesz. Így az or
, első operandusaként
az argumentumot használva, az ő értékét adja vissza ha az argumentumot megadtuk. Ellenkező
esetben a második operandust kapjuk vissza, ami így default argumentumértékként viselkedik.
Óvatosnak kell lennünk azonban boolean típusú argumentumok esetén. Az alábbi példa is mutatja, hogy ilyenkor nem nem feltétlen azt a viselkedést kapjuk, amit a valódi default argumentum értékektől elvárnánk:
Hasonló módon lehetséges a feltételes értékadás szimulációja logikai operátorokkal. Erre megint csak az operátorok sajátos szemantikája adja az alapot:
A fenti utasítás x
-et növeli, ha az kisebb, mint n
, különben
önmagát kapja értékül. Mivel az and
operátor precedenciája nagyobb, először
azt értékeljük ki. Az operátor működéséből adódóan x + 1
-gyet ad vissza, ha a
feltétel teljesül, különben false
-t. Ezután az or
, igaz feltétel esetén
az x + 1
-et adja vissza, ha a feltétel igaz volt, hiszen az összeg nem
false
vagy nil
. Ha a feltétel hamis volt (így az első operandus is az),
a mádosik operandussal, vagyis x
-szel tér vissza.
A fenti formula használata egy esetben nagyon kellemetlen eredményeket hozhat: ha az and
után false
vagy nil
érték következik, akkor mindig a második értéket
kapjuk vissza, a feltétel igazságértékétől függetlenül, hiszen a konjunkció eredménye hamis lesz.
Így ahogy a default argumentumértékek, a feltételes értékadás is csak részleges megoldásnak
tekinthető, amelyet körültekintően szabad csak használnunk.
A Lua automatikus memóriakezelést végez, azaz a programozónak nem kell foglalkoznia a memóriafoglalással és a memória felszabadításával akkor, mikor már az objektumaira nincs tovább szüksége. A memória felszabadítását a szemétgyűjtő - Garbage collector - végzi. Ez a mechanizmus időről időre begyűjt minden "halott" objektumot, azaz olyan objetumot, amelyhez már nincs hozzáférése a programnak. Ez minden Lua objektumra vonatkozik: táblák, userdata, sztringek, függvények, szálak stb.
A szemétgyűjtés periodikusan történik. Ehhez a Lua két számot használ fel. Az egyik azt mutatja, hogy mennyi dinamikus memóriát foglalt le a program. A másik pedig a határértéket. Amikor az első szám átlépi a másodikat, azaz a lefoglalt dinamikus memóra több lesz a határértéknél, akkor a Lua elindítja a szemétgyűjtőt és felszabadítja a már "halott" objektumok által lefoglalt memóriát. Ezután az első számértéket átírja az új értékre.
Lehetősége van ezen számok kezelésére a programozónak. Azaz lekérdezhetjünk mindkét értéket illetve a határértéket meg is változtathatjuk.
Ha a határértéket nullára állítjuk, akkor a szemétgyűjtés folyamatosan menni fog, ami viszont a program hatékonyságát jelentősen leronthatja.
A két értéket a gcinfo()
fügvénnyel kérdezhetjük le. A collectgarbage([limit])
fügvénnyel pedig beállíthatjuk
a határértéket.
A finalizer segítségével nemcsak a Lua szemétgyűjtőjét manipulálhatjuk, hanem az erőforrásokat is (például fájlok
lezárásakor, adatbázis vagy hálózati kapcsolatok kezelésekor). Azokat a userdata-kat, amelyek már felszabadultak, de használtuk a
__gc
mezőt a metatáblájukban, a Lua nem gyűjti be közvetlenül, hanem egy verembe kerülnek. Miután lezajlott a szemétgyűjtés,
egy ezzel a kóddal megyegyező függvény fut le:
A végén a finalizer fordított sorrendben kiveszi a veremből az ott letárolt objektumokat.
További információk a szemétgyűjtésről a 13. fejezetben olvashatók.