Coroutinen in Lua bieten eine äußerst leistungsfähige Möglichkeit, um komplexe, asynchrone Programmierungen zu handhaben, ohne auf traditionelle Threads oder Rückruffunktionen zurückzugreifen. Durch die Fähigkeit, den Ablauf zu unterbrechen und später fortzusetzen, erlauben Coroutinen eine sequentielle Programmierung, die den Eindruck erweckt, dass die Ausführung ohne Blockierungen erfolgt. Dies macht sie besonders nützlich für Szenarien, in denen mehrere, unabhängige Sequenzen von Operationen parallel ausgeführt werden müssen.

Ein zentrales Merkmal von Coroutinen ist ihre Fähigkeit, Ausführungen zu „pausieren“ und später wieder fortzusetzen. Ein gängiges Beispiel ist die Verwaltung von Zustandsmaschinen oder Iteratoren. Eine Coroutine lässt sich jederzeit anhalten, indem sie mit coroutine.yield() den aktuellen Zustand an den Aufrufer zurückgibt. Anschließend kann der Programmfluss durch einen erneuten Aufruf von coroutine.resume() fortgesetzt werden. Dies erlaubt eine saubere, sequentielle Handhabung von Aufgaben, ohne dabei auf die Komplexität von Threads oder Blockiermechanismen zurückgreifen zu müssen.

Ein häufiges Beispiel für die Verwendung von Coroutinen ist die Erstellung von Iteratoren. Durch den Einsatz von coroutine.yield() können Sie eine benutzerdefinierte Schleife erstellen, die bei jedem Schritt pausiert und nur dann fortgesetzt wird, wenn ein neuer Wert benötigt wird. Dies ist besonders bei großen Datensätzen oder komplexen Berechnungen nützlich, die nicht in einem einzigen Schritt abgearbeitet werden können.

In einem einfacheren Beispiel haben wir eine Funktion, die eine Reihe von Zahlen generiert. Die Coroutine wird in einem ersten Schritt mit der Zahl 1 pausiert und gibt sie an den Hauptprozess zurück. Bei jedem weiteren Aufruf wird die Ausführung fortgesetzt, sodass die nächste Zahl generiert wird. Erst wenn der gesamte Vorgang abgeschlossen ist, gibt die Coroutine eine Abschlussnachricht zurück, die den endgültigen Zustand beschreibt.

Ein anderes praktisches Beispiel zeigt, wie Coroutinen in einer Zustandsmaschine verwendet werden können. Hier wird die Ausführung in verschiedene Zustände unterteilt: einen „Initialzustand“, einen „Verarbeitungszustand“ und einen „Endzustand“. Durch die Übergabe von Daten in die Coroutine wird diese zwischen den Zuständen hin- und herbewegt, wobei bei jedem Übergang von einem Zustand zum nächsten relevante Informationen zurückgegeben werden. Diese Art der Steuerung ist besonders dann vorteilhaft, wenn komplexe Prozesse wie das Warten auf Benutzereingaben oder das Verarbeiten von Daten Schritt für Schritt verwaltet werden müssen, ohne dass der Hauptprozess blockiert wird.

Wenn eine Coroutine ihre Arbeit abgeschlossen hat, kann ein erneuter Aufruf von coroutine.resume() mit der bereits beendeten Coroutine zu einem Fehler führen. Dies tritt ein, weil Coroutinen nur dann fortgesetzt werden können, wenn sie sich noch in einem „aktiven“ Zustand befinden. Ist die Coroutine jedoch bereits abgeschlossen, liefert coroutine.resume() einen Fehler zurück, der darauf hinweist, dass die Coroutine nicht mehr fortgesetzt werden kann.

Ein weiteres Beispiel, das die Vielseitigkeit von Coroutinen unterstreicht, ist ihre Anwendung im Bereich der asynchronen Programmierung. In einer realen Anwendung könnte eine Coroutine für das Laden von Daten verwendet werden, ohne dass der Hauptprogrammfluss blockiert wird. Ein Loader-Objekt könnte so programmiert werden, dass es beim Laden eines Assets die Ausführung an der Stelle pausiert, an der das Asset noch nicht verfügbar ist. Der Hauptloop könnte derweil andere Aufgaben durchführen, und sobald das Asset fertig geladen ist, wird die Coroutine wieder fortgesetzt, um die restlichen Operationen durchzuführen. Diese Art der Handhabung ermöglicht eine reibungslose und nicht blockierende Ausführung von Aufgaben in einem ansonsten blockierenden Kontext.

Zusätzlich zu den genannten Anwendungsfällen ermöglichen Coroutinen auch eine feingranulare Kontrolle über den Ablauf von Programmen, die sich durch komplexe Logiken oder langwierige Berechnungen ziehen. In vielen modernen Anwendungen, insbesondere in der Spieleentwicklung oder bei Netzwerkoperationen, können Coroutinen eine hohe Effizienz und Lesbarkeit erzielen, da sie eine einfache Möglichkeit bieten, mit Parallelität zu arbeiten, ohne in die Komplexität von Threads oder komplizierten Callback-Systemen einzutauchen.

Was weiter zu beachten ist: Coroutinen sind besonders nützlich in Situationen, in denen traditionelle, synchrone Programmieransätze zu unübersichtlichem Code führen würden. Besonders bei der asynchronen Programmierung wird ihre Fähigkeit, den Programmkontrollfluss ohne Blockierungen zu steuern, zu einem unschätzbaren Werkzeug. Dabei ist es jedoch wichtig zu verstehen, dass der effektive Einsatz von Coroutinen nicht immer trivial ist, insbesondere bei sehr komplexen Zustandsmaschinen oder in Szenarien mit vielen gleichzeitig laufenden Coroutinen. In diesen Fällen kann eine sorgfältige Verwaltung der Lebenszyklen von Coroutinen erforderlich sein, um unerwartete Nebeneffekte oder Fehler zu vermeiden.

Wie Closures und anonyme Funktionen in Lua die Modularität und Flexibilität des Codes verbessern

In der Programmierung ist das Verständnis von Closures und anonymer Funktionen entscheidend für die Entwicklung modularer und flexibler Software. Besonders in der Programmiersprache Lua bieten diese Konzepte enorme Möglichkeiten, den Code effizient und übersichtlich zu gestalten. In diesem Zusammenhang wird die Bedeutung von Closures und anonymer Funktionen deutlich, wenn es darum geht, den Zustand und das Verhalten von Programmen zu trennen, um die Wartbarkeit zu erhöhen und Designmuster wie Fabriken oder Memoisierung umzusetzen.

Ein Closure in Lua entsteht, wenn eine Funktion in einem anderen Funktionskontext definiert wird und auf Variablen zugreift, die im äußeren Funktionsbereich definiert sind. Diese Funktion „erinnert sich“ an ihre Umgebung und kann sogar nach der Beendigung der äußeren Funktion weiterhin auf die Variablen zugreifen. Ein einfaches Beispiel ist das Erstellen eines Multiplikators durch eine Funktion:

lua
local function createMultiplier(multiplier)
return function(value) return value * multiplier end end local multiplyByTwo = createMultiplier(2) local multiplyByTen = createMultiplier(10) print(multiplyByTwo(5)) -- Ausgabe: 10 (5 * 2) print(multiplyByTen(7)) -- Ausgabe: 70 (7 * 10)

In diesem Beispiel erzeugt createMultiplier eine Funktion, die einen Wert mit einem vorab definierten multiplier multipliziert. Der Rückgabewert ist eine Funktion, die den multiplier speichert und später verwendet, was typisch für die Funktionsweise von Closures ist. Jeder Aufruf von createMultiplier erzeugt eine neue Schließung mit ihrem eigenen multiplier-Wert. Die Möglichkeit, dass eine Funktion ihren "Umfeldzustand" bewahren kann, ist ein wichtiges Merkmal der funktionalen Programmierung und wird in Lua häufig verwendet.

Diese Technik kann dabei helfen, Daten und Verhaltensweisen so zu organisieren, dass sie miteinander gekoppelt sind und so den Code modularer und wartbarer machen. Ein weiteres Beispiel für die Nutzung von Closures ist das Erstellen von Zählern, die ihren eigenen Zustand verwalten:

lua
function makeCounter()
local count = 0 return function() count = count + 1 return count end end local counter1 = makeCounter() print(counter1()) -- Ausgabe: 1 print(counter1()) -- Ausgabe: 2

Hier ist die counter1-Funktion ein Closure, das die lokale count-Variable aus der makeCounter-Funktion bewahrt, auch nachdem die äußere Funktion beendet wurde. Auf diese Weise kann für jedes makeCounter-Objekt ein eigener, unabhängiger Zustand erzeugt werden.

Ein weiteres sehr nützliches Konzept in Lua ist die Möglichkeit, Funktionen inline zu definieren, also ohne sie vorher explizit zu benennen. Dies geschieht häufig in Kombination mit Funktionen wie table.sort, die es ermöglichen, eine Vergleichsfunktion direkt im Funktionsaufruf zu definieren:

lua
local people = { {name = "Alice", age = 30}, {name = "Bob", age = 25}, {name = "Charlie", age = 35} }
table.sort(people, function(personA, personB)
return personA.age < personB.age end) for _, person in ipairs(people) do print(person.name .. " ist " .. person.age .. " Jahre alt.") end

In diesem Fall wird die anonyme Funktion als Argument für table.sort übergeben und direkt innerhalb des Aufrufs definiert. Diese Technik fördert nicht nur die Lesbarkeit, sondern reduziert auch den Aufwand, separate, benannte Funktionen zu erstellen, was den Code klarer und kürzer macht.

Anonyme Funktionen sind auch in der Programmierung von Benutzeroberflächen und beim Umgang mit Ereignissen von entscheidender Bedeutung. Sie werden häufig verwendet, um spezifische Aktionen für bestimmte Ereignisse zu definieren, wie z. B. das Klicken auf einen Button:

lua
function createButton(text, onClickCallback)
print("Erstelle Button mit Text: " .. text) -- Im echten Szenario würde dies ein UI-Element erzeugen -- und den onClickCallback registrieren print("Simuliere Klick für Button: " .. text) onClickCallback() end createButton("Klick mich!", function() print("Button wurde geklickt! Aktion ausführen.") end)

In diesem Beispiel wird die anonyme Funktion direkt als Rückruf (Callback) für das Ereignis des Button-Klicks übergeben. Diese Technik hilft dabei, die Funktionalität eng mit dem Ereignis zu verbinden, wodurch der Code leichter verständlich und wartbar bleibt.

Zusätzlich zur Flexibilität, die durch anonyme Funktionen erreicht wird, trägt diese Vorgehensweise zur Förderung eines deklarativen Programmierstils bei. Anstatt detailliert zu beschreiben, wie eine Aufgabe in mehreren Schritten ausgeführt werden soll, ermöglicht das Inline-Definition von Funktionen, dass das gewünschte Ergebnis präzise und direkt ausgedrückt wird.

Ein weiteres Beispiel für eine komplexere Verwendung anonymer Funktionen ist die Definition eines Iterators, der eine benutzerdefinierte Datenstruktur durchläuft:

lua
local mySequence = {first = 10, second = 20, third = 30}
local sequenceKeys = {"first", "second", "third"} function createSequenceIterator(data, keys) local index = 0 return function() index = index + 1 if index <= #keys then local key = keys[index] return key, data[key] else return nil end end end for k, v in createSequenceIterator(mySequence, sequenceKeys) do print("Schlüssel: " .. k .. ", Wert: " .. v) end

In diesem Beispiel wird die anonyme Funktion direkt im Rückgabewert von createSequenceIterator definiert. Sie verwaltet den Zustand des Iterators und liefert für jede Iteration das nächste Element. Diese Technik ermöglicht eine elegante, einfache und modulare Iteration über benutzerdefinierte Datenstrukturen.

Die Beherrschung von Closures und anonymen Funktionen ist entscheidend, um das volle Potenzial der Funktionsweise von Lua auszuschöpfen. Sie ermöglicht nicht nur eine kompaktere und verständlichere Syntax, sondern trägt auch zu einem stärker modularen Design bei, das die Wartbarkeit des Codes deutlich verbessert. Das Verständnis dieser Konzepte ist ein wesentlicher Schritt auf dem Weg, elegante, effiziente und wartbare Software in Lua zu entwickeln.

Warum anonyme Funktionen in Lua so mächtig sind

In Lua bietet die Möglichkeit, Funktionen inline zu definieren, eine beeindruckende Flexibilität und Ausdruckskraft, die den Code nicht nur kürzer, sondern auch lesbarer und wartungsfreundlicher machen kann. Diese Technik, oft als anonyme Funktionen bezeichnet, ermöglicht es, kleine, selbstständige Codeblöcke zu erstellen, die sofort ausgeführt oder als Argumente an andere Funktionen übergeben werden können. Ein tieferes Verständnis dieser Technik ist entscheidend für die Arbeit mit Lua und ihrer funktionalen Programmierung.

Die Syntax für anonyme Funktionen ist der für benannte Funktionen sehr ähnlich, jedoch ohne den Funktionsnamen. Stattdessen wird die Funktion direkt in einer Ausdrucksform genutzt, oft umgeben von Klammern, wenn sie Teil eines größeren Ausdrucks ist. Das Erstellen von anonymen Funktionen ist besonders nützlich, wenn eine Funktion für eine sehr spezifische Aufgabe benötigt wird, wie etwa ein Callback für ein Ereignis oder eine Transformation, die auf die Elemente einer Liste angewendet wird. Anstatt eine separate benannte Funktion zu definieren und diese dann zu referenzieren, kann die Funktion direkt dort definiert werden, wo sie benötigt wird. Dies fördert die Lokalisierung des Codes und erleichtert das Verständnis, da zusammenhängende Logik nah beieinander bleibt.

Nehmen wir als Beispiel eine einfache Tabelle von Zahlen, bei der jede Zahl verdoppelt werden soll. Anstatt eine separate Funktion zu schreiben und dann in einer Schleife aufzurufen, kann die Verdopplungslogik als anonyme Funktion direkt innerhalb der Schleife definiert werden. Dadurch wird der Code kompakter und fokussierter.

lua
local numbers = {1, 2, 3, 4, 5} local doubled_numbers = {} for i, num in ipairs(numbers) do
local doubler = function(x) return x * 2 end
table.insert(doubled_numbers, doubler(num)) end for _, val in ipairs(doubled_numbers) do print(val) end

Hier sehen wir die anonyme Funktion function(x) return x * 2 end als Zuweisung an die Variable doubler. Diese Funktion wird in der Schleife verwendet, um jedes Element der Tabelle zu verdoppeln. Obwohl die Funktion in einer Variablen gespeichert ist, könnte sie ebenso direkt im table.insert-Befehl genutzt werden.

Die wahre Stärke von anonymen Funktionen zeigt sich jedoch, wenn sie als Argumente an andere Funktionen übergeben werden. In Lua sind Funktionen „first-class citizens“, was bedeutet, dass sie wie andere Datentypen behandelt werden können – sie können in Variablen gespeichert, in Tabellen abgelegt und an andere Funktionen übergeben werden. Diese Eigenschaft bildet die Grundlage für funktionale Programmiermuster und eröffnet leistungsstarke Möglichkeiten.

Ein weit verbreitetes Beispiel für diese Technik ist das Konzept der „höheren Funktionen“, die eine Funktion als Argument akzeptieren. Solche Funktionen können die übergebene Funktion ausführen, ihren Rückgabewert nutzen oder ihr Verhalten sogar modifizieren. Ein einfaches Beispiel für eine höhere Funktion ist eine Funktion, die eine Aktion auf jedes Element einer Liste anwendet. Statt die Aktion direkt innerhalb der Funktionsdefinition festzulegen, kann die Funktion so gestaltet werden, dass sie eine andere Funktion als Argument akzeptiert, die die auszuführende Aktion bestimmt.

lua
local function applyToEach(tbl, func) local result_table = {} for i, value in ipairs(tbl) do table.insert(result_table, func(value)) end return result_table end
local numbers = {1, 2, 3, 4, 5}
-- Definiere eine anonyme Funktion, um eine Zahl zu quadrieren local squared_numbers = applyToEach(numbers, function(n) return n * n end)
for _, val in ipairs(squared_numbers) do
print(val) end

In diesem Beispiel nimmt die Funktion applyToEach eine Tabelle und eine Funktion als Eingabewerte. Diese Funktion wendet die übergebene Funktion (func) auf jedes Element der Tabelle an. Wir definieren die Funktion, die das Quadrat einer Zahl berechnet, direkt in der applyToEach-Funktion und übergeben sie als Argument. Dies spart nicht nur Code, sondern macht die Funktion auch flexibler und wiederverwendbarer.

Ein weiteres häufiges Anwendungsszenario für anonyme Funktionen ist das Event-Handling. Viele Frameworks und Bibliotheken setzen auf ein Callback-System, bei dem eine Funktion übergeben wird, die ausgeführt wird, wenn ein bestimmtes Ereignis eintritt, wie etwa ein Klick auf einen Button oder das Ablaufen eines Timers.

lua
local function onButtonClick(callback_function)
print("Button clicked!") callback_function() end -- Definiere, was bei einem Button-Klick passieren soll, mit einer anonymen Funktion onButtonClick(function() print("Custom action: Displaying a message.") end) -- Ausgabe: -- Button clicked! -- Custom action: Displaying a message.

In diesem Beispiel erwartet die Funktion onButtonClick eine Funktion als Argument, die bei einem Button-Klick ausgeführt wird. Die Flexibilität zeigt sich hier darin, dass unterschiedliche anonyme Funktionen übergeben werden können, um verschiedene Aktionen auszulösen, ohne die onButtonClick-Funktion selbst ändern zu müssen. Dieses Muster trägt dazu bei, den Code modular und erweiterbar zu halten.

Eine ähnliche Flexibilität bieten Funktionen, die für Sortieroperationen verwendet werden. In Lua erlaubt die Funktion table.sort, eine benutzerdefinierte Vergleichsfunktion zu übergeben, die festlegt, wie die Elemente sortiert werden sollen. Dies wird besonders nützlich, wenn die Sortierung nach benutzerdefinierten Kriterien erfolgen soll.

lua
local people = { {name = "Alice", age = 30}, {name = "Bob", age = 25}, {name = "Charlie", age = 35} } -- Sortiere nach Alter table.sort(people, function(a, b) return a.age < b.age end) print("Sorted by age (ascending):") for _, person in ipairs(people) do print(person.name, "-", person.age) end

In diesem Beispiel verwenden wir eine anonyme Funktion als zweiten Parameter der table.sort-Funktion, um die Personen nach Alter zu sortieren. Diese Technik zeigt eindrucksvoll, wie die Übergabe von Funktionen als Argumente eine maßgeschneiderte Logik in generischen Operationen ermöglicht.

Die Fähigkeit, Funktionen als Argumente zu übergeben und inline zu definieren, steigert die Modularität und Lesbarkeit des Codes erheblich. Sie erlaubt es, Programme zu erstellen, die flexibler, wiederverwendbarer und leichter zu warten sind. Gerade in der funktionalen Programmierung eröffnen sich durch diesen Mechanismus viele neue Möglichkeiten für die Entwicklung von Anwendungen, die sauberer und eleganter gestaltet sind.

Wie funktioniert Vererbung in Lua und wie nutzt man Metatabellen für die Objektorientierung?

In Lua, einer Sprache, die keinen eingebauten Mechanismus für objektorientierte Programmierung bietet, können wir trotzdem eine Art Vererbung implementieren, indem wir Metatabellen und sogenannte Metamethoden nutzen. Eine der zentralen Techniken hierbei ist die Verwendung der __index Metamethode, um auf andere Tabellen oder Prototypen zu verweisen, was eine Delegation von Methoden und Eigenschaften ermöglicht.

Um ein einfaches Beispiel zu verstehen, beginnen wir mit einem generischen Shape-Prototypen, der grundlegende Methoden wie move und draw enthält. Die Tabelle für den Shape-Prototyp sieht folgendermaßen aus:

lua
local Shape = {} Shape.x = 0 Shape.y = 0 function Shape:move(dx, dy) self.x = self.x + dx self.y = self.y + dy
print(string.format("Moving shape to (%d, %d)", self.x, self.y))
end function Shape:draw() print(string.format("Drawing a generic shape at (%d, %d)", self.x, self.y)) end

In diesem Beispiel haben wir eine allgemeine Methode move, die die Position eines Objekts verändert, und eine draw-Methode, die nur einen Platzhalter darstellt. In einem typischen objektorientierten Ansatz würden Unterklassen diese draw-Methode überschreiben, um spezifische Zeichnungsoperationen zu implementieren.

Um nun eine Unterklasse zu erstellen, beispielsweise für einen Circle (Kreis), definieren wir eine Konstruktorfunktion, die von Shape erbt. Der entscheidende Punkt dabei ist, dass die Metatabelle des Circle-Objekts auf den Shape-Prototyp verweist, sodass Methoden, die im Shape definiert sind, auch für den Circle verfügbar sind, sofern sie nicht überschrieben werden.

lua
local Circle = {}
Circle.radius = 1 function Circle.new(x, y, radius) local instance = {} instance.x = x or 0 instance.y = y or 0 instance.radius = radius or 1 local metatable = { __index = Shape } setmetatable(instance, metatable) return instance end function Circle:draw() print(string.format("Drawing a circle at (%d, %d) with radius %d", self.x, self.y, self.radius)) end

Der Kreis überschreibt die draw-Methode, behält jedoch die move-Methode aus dem Shape-Prototyp. Wenn wir nun ein Circle-Objekt erstellen und die Methode move aufrufen, wird die move-Methode des Shape-Prototyps ausgeführt. Wenn jedoch draw aufgerufen wird, wird die spezifische Methode für Circle verwendet.

lua
local myCircle = Circle.new(10, 20, 5)
myCircle:move(2, 3) -- Ruft Shape:move auf myCircle:draw() -- Ruft Circle:draw auf

Dies verdeutlicht, wie die Vererbung in Lua funktioniert: Lua durchsucht die Metatabelle und findet die Methode move im Shape-Prototyp, da Circle sie nicht definiert hat. Für die draw-Methode ist die Reihenfolge umgekehrt, da Circle sie überschreibt.

Nun, wenn wir die Vererbung weiter verfeinern möchten, etwa indem wir ein anderes Objekt wie ein Rechteck hinzufügen, das ebenfalls von Shape erbt, gehen wir genauso vor. Ein Rectangle könnte ebenfalls die Methode draw überschreiben, während es die move-Methode des Shape-Prototyps behält.

lua
local RectangleClass = {} RectangleClass.width = 10 RectangleClass.height = 5 function RectangleClass:draw() print(string.format("Drawing a rectangle at (%d, %d) with width %d and height %d", self.x, self.y, self.width, self.height)) end local RectangleClassMetatable = { __index = Shape } setmetatable(RectangleClass, RectangleClassMetatable) function RectangleClass.new(x, y, width, height) local instance = Shape.new(x, y) instance.width = width or 10 instance.height = height or 5 local metatable = { __index = RectangleClass } setmetatable(instance, metatable) return instance end
local myRectangle = RectangleClass.new(30, 40, 15, 25)
myRectangle:move(
5, -5) -- Ruft Shape:move auf myRectangle:draw() -- Ruft RectangleClass:draw auf

Mit diesem Setup sehen wir, dass RectangleClass die Methode draw überschreibt und weiterhin die Methoden von Shape nutzen kann, die in der Vererbungskette weiter oben definiert sind. Auch hier zeigt sich das Prinzip der Delegation: das Objekt wird durch die Metatabelle mit der entsprechenden Methode verbunden.

Zusätzlich zur grundlegenden Vererbung ist es auch möglich, die __newindex-Metamethode zu nutzen, um das Verhalten bei der Zuweisung von neuen Eigenschaften zu beeinflussen. Diese Methode hilft dabei, sicherzustellen, dass Eigenschaften nur auf dem konkreten Objekt und nicht auf dem Prototyp selbst gesetzt werden, wodurch eine strikte Trennung der Instanz-Eigenschaften gewährleistet wird.

lua
local ShapeWithNewIndex = {} ShapeWithNewIndex.x = 0 ShapeWithNewIndex.y = 0 function ShapeWithNewIndex:move(dx, dy) self.x = self.x + dx self.y = self.y + dy
print(string.format("Moving shape to (%d, %d)", self.x, self.y))
end function ShapeWithNewIndex.__newindex(self, key, value) if key == "x" or key == "y" then rawset(self, key, value) else error("Cannot modify property " .. key) end end

Das hier gezeigte Beispiel zeigt, wie man verhindern kann, dass zufällige oder ungewollte Änderungen an den Instanz-Eigenschaften vorgenommen werden, indem man die Zuweisung von Eigenschaften genau steuert.

Metatabellen und die __index- und __newindex-Metamethoden ermöglichen also eine starke Flexibilität und Kontrolle über das Verhalten von Objekten in Lua und bieten eine Möglichkeit, eine eigene objektorientierte Struktur aufzubauen. Sie ermöglichen es, eine Hierarchie von Klassen zu erstellen und die Vererbung ähnlich wie in klassischen objektorientierten Sprachen umzusetzen.