Programmieren lernen mit Lua: Lektion 05b: Tabellen als Objekte

Übersicht | Vorherige Lektion




In den bisherigen Beispielen dieser Lektion haben wir lediglich Strings in Tabellen gespeichert. Es lassen sich aber grundsätzlich Werte jeden Typs in einer Tabelle speichern: Zahlen, Booleans, andere Tabellen und auch Funktionen.

Tabellen, die Funktionen enthalten, sind in den vorherigen Lektionen bereits häufiger vorgekommen:

io.read()
math.random()
table.remove()
table.insert()

io, math und table sind sogenannte Librarys (also Bibliotheken), die Funktionen zu jeweils einem bestimmten Aufgabengebiet versammeln und bereitstellen. Das folgende Beispiel zeigt, wie ihr selbst Funktionen in eine Tabelle „verpacken“ könnt:

dialog = {

  begruessung = function() 
    print("Hallo, wie gehts?")
  end,

  abschied = function()
    print("Tschüss, mach's gut!")
  end

}

Das Programm auf repl.it

Exkurs: Schreibweisen von Funktionsdefinitionen

Die Schreibweise der Funktionsdefiniton innerhalb der Tabelle bedarf der Erläuterung. Üblicherweise definieren wir in Lua Definitonen auf folgende Weise:

function gruss()
  print("guten tag!")
end

Tatsächlich ist das eine Abkürzung für folgendes:

gruss = function()
  print("guten tag!")
end

Der Ausdruck rechts vom = liefert eine Funktion, die dem Bezeichner gruss zugewiesen wird.

Wenn ihr eine Funktion innerhalb einer Tabelle definieren wollt, funktioniert nur die letztgenannte Schreibweise.

Anwendung der Funktionen in der Tabelle dialog

Die Benutzung der Funktionen aus der Tabelle dialog sieht aus wie folgt – ganz genau so wie bei io.read() oder math.random():

$ dialog.begruessung()
Hallo, wie geht's?
$ dialog.abschied()
Tschüss, mach's gut!

Man könnte die Funktion auch so aufrufen – unnötig kompliziert, aber inhaltlich korrekt:

dialog["begruessung"]()

Weil genau das im Hintergrund passiert:

"Nimm das, was unter dem Schlüssel begruessung in der Tabelle dialog gespeichert ist
und rufe es als Funktion auf."

Aufgabe 4: Füge der Tabelle dialog eine weitere Funktion smalltalk() hinzu, die bei Aufruf eine Gesprächsfloskel wie „Schönes Wetter heute.“ ausgibt. Teste das Programm in der Konsole.

dialog = {

  begruessung = function() 
    print("Hallo, wie gehts?")
  end,

  smalltalk = function()
    print("Schönes Wetter heute!")
  end,

  abschied = function()
    print("Tschüss, mach's gut!")
  end

}

Die Lösung auf repl.it

Aufgabe 5: Gestalte die Dialog-Funktionen etwas persönlicher: Bei dialog.begruessung() soll ein Name als Argument übergeben und in den Gruss eingebaut werden:

$ dialog.begruessung("Maria")
Hallo Maria, wie geht's?
$ dialog.abschied("Maria")
Tschüss Maria, mach's gut!

dialog = {

  begruessung = function(name) 
    print("Hallo " .. name .. ", wie gehts?")
  end,

  smalltalk = function()
    print("Schönes Wetter heute!")
  end,

  abschied = function(name)
    print("Tschüss " .. name .. ", mach's gut!")
  end

}

Die Lösung auf repl.it

Aufgabe 6: math ist eine Tabelle, die Funktionen enthält – wir haben sie bereits in Lektion 2 verwendet, um mit math.random() Zufallszahlen zu erzeugen. Kannst Du die Namen aller Funktionen ausgeben, die in math enthalten sind?

for name in pairs(math) do
  print(name)
end

Anmerkung: wir benötigen hier von jedem Eintrag in der Tabelle math nur den jeweils ersten Rückgabewert von pairs().

Die Lösung auf repl.it

Tabellen als Objekte

In der Informatik steht der Begriff Objekt für eine Zusammenfassung von zusammengehörigen Eigenschaften und Funktionen. In vielen Situationen ist es sinnvoll, die Aufgaben innerhalb eines Programms auf solche Objekte zu verteilen. Objekte können etwa Raumschiffe oder feindliche Aliens in einem Computerspiel sein. Oder, weniger spektakulär: Kundenkonten oder Produkte in einem Online-Shop. In diesem letzten Abschnitt wollen wir skizzieren, wie man in Lua „objektorientiert“ programmiert. Das Thema ist recht anspruchsvoll. Anfänger sollten sich nicht zu schnell entmutigen lassen, wenn beim ersten Lesen nicht alles klar wird.

Ein einfacher Punktezähler

Um ein konkretes Beispiel zu nennen: Stellen wir uns vor, wir wollen für ein Computerspiel einen ganz einfachen Punktezähler programmieren, der lediglich zwei Eigenschaften hat:

  • Den Namen des Spielers
  • Die Anzahl der Punkte

Zudem soll es zwei Funktionen geben:

  • Die Punktzahl um 1 erhöhen
  • Den aktuellen Punktestand in schöner Form angeben

Das lässt sich in Lua leicht mit einer Tabelle umsetzen:

p = {

  name = "Sigrid",

  punkte = 0,

  punkten = function()
    p.punkte = p.punkte + 1
  end,

  drucken = function()
    print("Punktestand " .. p.name .. ": " .. p.punkte)
  end

}

Zeile 3: Die Eigenschaft name
Zeile 5: Die Eigenschaft punkte, initialisiert mit 0
Zeilen 7-9: Die Funktion punkten(): p.punkte verweist auf den in der Tabelle gespeicherten Punktestand.
Zeilen 11-13: Die Funktion drucken(): p.name verweist auf den in der Tabelle gespeicherten Namen.
Obacht:: Die einzelnen Elemente in einer Tabelle sind immer durch Kommas getrennt. Vergessene Kommas sind eine sehr häufige Fehlerquelle, auch unter Profis.

Eine Anwendung auf der Konsole sieht dann so aus. Jeder Aufruf der Funktion punkten() erhöht den Punktestand um 1:

$ p.drucken()
Punktestand Sigrid: 0
$ p.punkten()
$ p.drucken()
Punktestand Sigrid: 1
$ p.punkten()
$ p.drucken()
Punktestand Sigrid: 2

Das Programm auf repl.it

Der Haken an der ersten Version des Punktezählers

Schaut euch den Code des Punktezähles noch einmal genau an: Die in der Tabelle p enthaltenen Funktionen punkten() und drucken() enthalten Verweise auf p, also auf die Tabelle selbst. Andernfalls könnten diese Funktionen die Eigenschaften von p (also name und punkte) nicht abfragen und verändern:

punkten = function()
  p.punkte = p.punkte + 1
end

So lange das Spiel nur von einer einzigen Person gespielt wird, ist das kein Problem. Was aber, wenn es mehrere Spieler geben sollte? Dann müsstet ihr für jeden Spieler den kompletten Code kopieren, und an allen Stellen, wo p steht (es sind 5!) p2 einsetzen. Das geht besser! Wie genau, zeigt das nächste Beispiel.

Der erste Schritt besteht darin, die Funktionen punkten() und drucken() unabhängig von einem bestimmten Punktezähler, einer bestimmten Tabelle zu machen. Zu diesem Zwecke brauchen die beiden Funktionen jeweils ein Argument, das auf den jeweiligen Punktezähler selbst verweist. Es ist in Lua üblich, dieses Argument self zu nennen.

function punkten(self)
  self.punkte = self.punkte + 1
end

function drucken(self)
  print("Punktestand " .. self.name .. ": " .. self.punkte)
end

Jetzt ist es im zweiten Schritt möglich, beliebig viele Punktezähler zu erzeugen, und ihnen diese allgemeine Form der beiden Funktionen hinzuzufügen:

p1 = {
  name = "Sigrid",
  punkte = 0,
  punkten = punkten,
  drucken = drucken,
}

p2 = {
  name = "Theodor",
  punkte = 0,
  punkten = punkten,
  drucken = drucken
}

Die Anwendung dieses modifizierten Punktezählers auf der Konsole könnte so aussehen:

$ p1.drucken(p1)
Punktestand Sigrid: 0

Dass wir nun aber p1 zweimal hinschreiben müssen, ist sehr unschön. Dafür hat Lua eine verkürzte Schreibweise anzubieten. Wenn wir statt eines Punktes einen Doppelpunkt schreiben, wird die Tabelle selbst automatisch als erstes Argument eingesetzt:

$ p1:drucken()
Punktestand Sigrid: 0

Das Programm auf repl.it

Aufgabe 7: Teste die beiden Punktezähler p1 und p2 auf der Konsole mit der vorgestellten Schreibweise. Überzeuge Dich davon, dass die beiden Zähler unabhängig voneinander sind.

Aufgabe 8: Füge den Punktezählern eine Funktion zuruecksetzen() hinzu, die den Punktestand auf 0 setzt. Teste die Funktion auf der Konsole.

function punkten(self)
  self.punkte = self.punkte + 1
end

function zuruecksetzen(self)
  self.punkte = 0
end

function drucken(self)
  print("Punktestand " .. self.name .. ": " .. self.punkte)
end

p1 = {
  name = "Sigrid",
  punkte = 0,
  punkten = punkten,
  zuruecksetzen = zuruecksetzen,
  drucken = drucken,
}

p2 = {
  name = "Theodor",
  punkte = 0,
  punkten = punkten,
  zuruecksetzen = zuruecksetzen,
  drucken = drucken
}

Die Lösung auf repl.it

Aufgabe 9: Füge dem Punktezähler eine weitere Funktion bonus() hinzu, die ein Argument betrag erwartet und den Punktestand um den Betrag erhöht. Teste die Funktion auf der Konsole.

function punkten(self)
  self.punkte = self.punkte + 1
end

function zuruecksetzen(self)
  self.punkte = 0
end

function bonus(self, betrag)
  self.punkte = self.punkte + betrag
end

function drucken(self)
  print("Punktestand " .. self.name .. ": " .. self.punkte)
end

p1 = {
  name = "Sigrid",
  punkte = 0,
  punkten = punkten,
  zuruecksetzen = zuruecksetzen,
  betrag = betrag,
  drucken = drucken,
}

p2 = {
  name = "Theodor",
  punkte = 0,
  punkten = punkten,
  zuruecksetzen = zuruecksetzen,
  betrag = betrag,
  drucken = drucken
}

Die Lösung auf repl.it

Es geht noch besser: Eine Fabrik für Punktezähler

Die zuletzt gezeigte Variante ist zwar schon ein gewaltiger Fortschritt im Vergleich zum ersten Versuch. Trotzdem müssen wir immer noch für jeden neuen Zähler jede Menge Code schreiben. Zu guter Letzt zeigen wir eine Variante, die noch knapper ist. Hier definieren wir eine Funktion, erzeuge_zaehler(), die jeweils einen kompletten Zähler zurückliefert. Eine Funktion, die Objekte produziert, nennt man übrigens auch Fabrik bzw. englischsprachig Factory:

function punkten(self)
  self.punkte = self.punkte + 1
end

function drucken(self)
  print("Punktestand " .. self.name .. ": " .. self.punkte)
end

function erzeuge_zaehler(name)
  
  return {
    name = name,
    punkte = 0,
    punkten = punkten,
    drucken = drucken,
  }

end

p1 = erzeuge_zaehler("Sigrid")
p2 = erzeuge_zaehler("Theodor")

Zeilen 1-7 definieren wie gehabt die beiden Funktionen punkten() und drucken().
Zeile 9-18 definieren die Factory-Funktion.
Zeile 9: Ein frischer Zähler soll sich nur durch den Namen unterscheiden – der wird hier per Argument geliefert.
Zeilen 11-16: Der Rückgabewert der Funktion ist ein komplettes Zähler-Objekt!
Zeile 20 und 21: Nunmehr genügt ein einzeiliger Befehl zur Erzeugung eines Zählers!

Was wir hier ausgelassen haben & Schlußwort

In den ersten vier Lektionen ist es (hoffentlich!) gelungen, einen Großteil der wichtigsten Infos zu dem jeweiligen Thema vorzustellen. Trotz des im Vergleich zu den vorherigen Lektionen doppelten Umfangs konnten wir das zum Thema Tabellen nicht leisten. Tabellen sind der mit Abstand wichtigste und leistungsfähigste Datentyp der Programmiersprache Lua. Gerade das letzte Beispiel hat hoffentlich deutlich gemacht, wie flexibel und vielgestaltig Tabellen einsetzbar sind. Wir konnten hier tatsächlich nur an der Oberfläche kratzen. Es lohnt sich, hier auf eigene Faust weiter zu lernen und zu experimentieren. Im folgenden liefern wir dazu ein paar Stichworte und Hinweise.

Verschachtelte Tabellen

Tabellen können Tabellen enthalten. Das kann zum Beispiel so aussehen:

schrank = {
    schachtel = {"murmel", "postkarte", "puppe"},
    koffer = {"hose", "jacke", "schuhe"}
  }
}

Eine witzige und lehrreiche Übung hierzu wäre, nach dem Vorbild unseres Zählers einen Behälter zu programmieren, mit einer inneren Tabelle für den Inhalt und drei Funktionen hineintun(), herausholen() und inhalt_drucken(), hier als Skizze:

behaelter = {
    inhalt = {"murmel", "postkarte", "puppe"},
    hineintun = ...
    herausholen = ...
    inhalt_drucken = ...
  }
}
Metatabellen

Das umfangreichste hier ausgelassene Thema betrifft die sogenannten Metatabellen. Jede Tabelle T lässt sich mit einer Metatabelle M verknüpfen. Die in M gespeicherten Funktionen können modifizieren, wie sich T in bestimmten Situationen verhält, etwa:

  • Wenn ein Schlüssel abgefragt wird, unter dem nichts eingetragen ist
  • Wenn unter einem Schlüssel erstmals etwas eingetragen wird
  • Wenn die Tabelle wie eine Funktion aufgerufen wird, also T()
  • Wenn eine Tabelle mit einer anderen Tabelle oder eine Wert addiert, multipliziert etc. wird

Sogar das Verhalten von pairs() und ipairs() lässt sich mittels der Metatabellen modifizieren.

Auf diese Weise lassen sich sehr nützliche und merkwürdige Verhaltensweisen programmieren. Etwa eine Tabelle, die sich von aussen so verhält, als ob sie unendlich viele Einträge hat; tatsächlich aber den jeweiligen Inhalt nur berechnet und für spätere Verwendung speichert, wenn er abgefragt wird. Oder es lässt sich mittels Tabellen einen Zahlentyp definieren, der normalerweise genau rechnet, nur bei 7 * 7 kommt „feiner Sand“ heraus. Kürzlich haben wir in einem Programmierkurs eine Engine für Text-Adventures geschrieben, bei der alle Räume und Gegenstände letztlich Tabellen waren.

Kurz und gut: Die Programmierung per Metatabellen ist höchst faszinierend und auch in der Familie von tausenden von Programmiersprachen ungewöhnlich. Es macht den Reiz von Lua aus, dass sich mittels geschickter Anordnung von Tabellen buchstäblich jedes Verhalten erreichen lässt, dass überhaupt von einem Computerprogramm erwartet werden Kann. Der Phantasie sind hier keine Grenzen gesetzt. Eine Beschäftigung damit lohnt sich unbedingt. Eine (leider nur in englscher Sprache) vorliegende Auflistung aller Metatabellen-Schlüssel findet ihr hier.

Quellen zum weiterlernen

2022 werden wir eine Fortsetzung dieses Kurses veröffentlichen, der das hier gezeigte vertieft und forgeschrittene Themen wie Metatabellen behandelt.

Bis dahin können wir die folgenden beiden freien online-Quellen zum weiterlernen empfehlen:

Programming in Lua first Edition (englisch)

Lua 5.1 Referenzhandbuch (deutsch)

Beide Bücher sind etwas technisch geschrieben, und richten sich eher an Personen mit Informatik-Kenntnissen. Aber wenn ihr diesen Kurs gemeistert habt, solltet ihr auch mit den Büchern zurechtkommen.

Navigation

Übersicht

Vorherige Lektion


Wir bedanken uns bei der Peakboard GmbH für die freundliche Unterstützung bei der Entwicklung dieses Kurses.

Peakboard ist eine All-in-One-Lösung aus Soft- und Hardware, mit der Du Daten aus unterschiedlichen Datenquellen erhebst, auswertest und in Echtzeit auf Bildschirmen visualisierst. Mit der kostenlosen Software, dem Peakboard Designer, gestaltest Du Dein individuelles Dashboard und bindest deine Datenschnittstellen an. Die Hardware, die Peakboard Box, verarbeitet und kommuniziert die Datenströme dezentral und damit ressourcenschonend direkt am Industriearbeitsplatz. Damit sorgst Du für mehr Transparenz und optimierst so ganz einfach deine Prozesse.