Lua: Metatabellen und objektorientierte Programmierung


< Funktionen
Gesamtübersicht (Start) / Seminarthemen WS 2009/10

Ü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 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)


< Funktionen
Zum Seitenanfang