Übersicht
Metatabellen
Metatabellen sind ein wichtiger Mechanismus in Lua. Sie dienen zum Überladen von Operatoren, aber sie bieten darüber hinaus noch weitere Funktionalität.In Lua haben alle Werte Metatabellen, allerdings können nur die Metatabellen von Tabellen bearbeitet werden. Tabellen können Metatabellen zugeordnet werden, die Operationen auf den Tabellen beschreiben, wie z.B. die Addition oder der Vergleich. So lassen sich eigene Datentypen umsetzen.
Standardmäßig hat eine Tabelle eine leere Metatabelle. Addiert man zwei Tabellen schaut Lua in der Metatabelle der Tabelle unter dem Schlüssel __add nach und führt die dort gespeicherte Funktion aus. Die Funktion bekommt die beiden Tabellen als Argumente übergeben.
Folgendes Beispiel zeigt dies am Beispiel von Tabellen, die Vektoren darstellen:
vec1 = {1,2,3}
vec2 = {6,4,8}
function vector_add(a, b)
assert(#a == #b, "Die Vektoren haben unterschiedliche Dimensionen!")
local res ={}
for index,_ in ipairs(a) do
res[index] = a[index] + b[index]
end
return res
end
function vector_tostring(vector)
local str = "("
for index, wert in ipairs(vector) do
str = str .. " " .. wert
if index < #vector then
str = str .. ","
end
end
str = str .. " )"
return str
end
mt = {__add = vector_add}
setmetatable(vec1, mt)
vec3 = vec1 + vec2
print(vector_tostring(vec1) .. " + " .. vector_tostring(vec2) .. " = " .. vector_tostring(vec3))
mt ist hier die Metatabelle mit einer Metamethode __add. Mit setmetatable wird sie der Tabelle vec1 zugeordnet.
Grundlagen der objektorientierten Programmierung
Objektorientierte Programmierung wird in Lua nur rudimentär durch syntaktische Erleichterungen unterstützt. Als Basis für Objekte dienen Tabellen, welche als Namensraum genutzt werden. Datenfelder und Methoden werden unter ihrem Namen als Schlüssel in einer Tabelle eingetragen.Folgendes Beispiel demonstriert, wie ein Objekt LKW erstellt wird. Es gibt ein Datenfeld, welches das Gewicht der Ladung angibt und eine Methode mit deren Hilfe Ladung aufgenommen werden kann.
lkw = {}
lkw.ladungsgewicht = 0
lkw.zuladen = function(l, gewicht)
l.ladungsgewicht = l.ladungsgewicht + gewicht
end
lkw.zuladen(lkw, 2500) -- 2500kg zuladen
An diesem Beispiel ist nicht optimal, dass man beim Methodenaufruf immer das Objekt (die Tabelle) angeben muss auf dem die Methode arbeiten soll. Für dieses Problem bietet Lua die erwähnte syntaktische Erleichterung, welche den ersten Parameter, also das Objekt auf dem die Methode arbeiten soll, implizit an die Methode übergibt.
lkw = {}
lkw.ladungsgewicht = 0
function lkw:zuladen(gewicht)
self.ladungsgewicht = self.ladungsgewicht + gewicht
end
lkw:zuladen(2500) -- 2500kg zuladen
Hier sorgt der Doppelpunkt dafür, dass Lua einen impliziten ersten Parameter mit dem Namen self der Methode zuladen hinzufügt. Über self kann eine Methode dann immer auf das Objekt zugreifen, für das die Methode aufgerufen wurde.
Klassen
Die Art und Weise wie im oberen Beispiel Objekte erzeugt werden, würde bedeuten, dass für jedes neue Objekt immer wieder die Datenfelder und Methoden in einer Tabelle angelegt werden müssten. Es fehlt noch ein Klassenkonzept, welches mit Hilfe von Metatabellen simuliert werden kann.Im Beispiel muss dem LKW Objekt, das als Klasse (Prototyp) dienen soll, eine Methode new hinzugefügt werden, welche neue LKW Objekte erzeugen wird. In dieser Methode wird eine leere Tabelle erzeugt, welche als Basis für das neue Objekt dient. Diese Tabelle bekommt nun den bereits existierenden LKW mit all seinen Datenfeldern und Methoden als Metatabelle. Außerdem wird in der Tabelle, die die Klasse repräsentiert und Metatabelle des neue LKWs ist, unter dem Schlüssel __index sich selbst eingetragen. Schließlich wird die noch leere Tabelle des neuen LKWs zurückgegeben. Das ganze sieht dann wie folgt aus:
lkw = {}
lkw.ladungsgewicht = 0
function lkw:zuladen(gewicht)
self.ladungsgewicht = self.ladungsgewicht + gewicht
end
function lkw:new()
local res = {}
setmetatable(res, self)
self.__index = self
return res
end
meinlkw = lkw:new()
Die folgende Grafik verdeutlicht die entstehende Struktur:
Die letzte Zeile in dem Beispiel erzeugt ein neues LKW Objekt mit dem Namen meinlkw, was zu diesem Zeitpunkt nichts anderes ist, als eine leere Tabelle mit dem existierenden LKW als Metatabelle, welche zudem sich selbst unter dem Schlüssel __index eingetragen hat. Ruft man nun z.B. meinlkw:zuladen(300) auf, so wird in der leeren Tabelle meinlkw der Schlüssel zuladen gesucht. Da dieser nicht vorhanden ist, wird in der Tabelle nachgesehen, die in der Metatabelle unter dem Schlüssel __index eingetragen ist. Im Ergebnis wird also die Methode zuladen aus der Tabelle lkw genutzt.
Über diesen Trick mit der Metatabelle hat meinlkw nun alle Methoden und Datenfelder von lkw. meinlkw kann sogar selbst wieder Objekte erzeugen.
Ändert sich ein Datenfeld, so wird der neue Wert in meinlkw gespeichert und von nun an auch dort gelesen und nicht mehr aus der Metatabelle.
Vererbung
Ein bedeutender Bestandteil von objektorientierter Programmierung nämlich die Vererbung fehlt jedoch noch. Darum erweitern wir jetzt unser Beispiel. Es soll nicht nur LKWs geben, sonder auch noch Kühllkws. Diese sollen alle Eigenschaften von "normalen" LKWs haben und zusätzlich noch eine Temeraturangabe für den Laderaum. Außerdem soll Ladung nur zugeladen werden können, wenn die maximale Lagertemperatur des Lagerguts geringer ist, als die Laderaumtemperatur des Kühllkws.Diese Anforderungen lassen sich durch kleine Ergänzungen des oberen Beispiels erreichen. Die new Methode, welche neue Obejekte erzeugt, bekommt als Paramter eine Tabelle, welche als Ausgangsbasis für das neue Objekt dienen soll. Alle zusätzlichen Datenfelder und Methoden für das neue Objekt kann man nun beim Aufruf von new übergeben. Im Beispiel wird die Methode zuladen schließlich auch noch mit einer neuen überschrieben. Das dies nicht beim AUfruf von new gemacht wird hat vorallem den Grund, die alternative Syntax zu demonstrieren.
lkw = {}
lkw.ladungsgewicht = 0
function lkw:zuladen(gewicht)
self.ladungsgewicht = self.ladungsgewicht + gewicht
end
function lkw:new(o)
local res = o or {}
setmetatable(res, self)
self.__index = self
return res
end
kuehllkw = lkw:new({temperatur = 0})
function kuehllkw:zuladen(gewicht, maxtemp)
if self.maxtmp < self.temperatur then
self.ladungsgewicht = self.ladungsgewicht + gewicht
return true
else
return false
end
end
-- Kühllkw mit einer Ladungstemperatur von 3°C erzeugen
meinkuehllkw = kuehllkw:new{temperatur = 3}
-- 800kg zuladen, die bei maximal 5°C gelagert werden dürfen
meinkuehllkw:zuladen(800, 5)