Yscript Klassen 2016-01-03
Bei der Klassenvererbung geht es darum, Klassen aufeinander aufzubauen, um Arbeit zu sparen, Code zu vereinfachen und dadurch Fehler zu vermeiden. Davon abgesehen lassen sich bestimmte Dinge ohne Vererbung kaum realisieren. Ein einfaches Beispiel: Es gibt eine Klasse "Container" (Behälter) mit der Fähigkeit, ein Item in sich aufzunehmen. Nun soll eine neue Klasse "Bottle" (Flasche) erstellt werden, welches im Grunde auch ein Behälter ist, aber die Flasche soll zusätzlich einen Verschluss haben.
Code-Beispiel:
Hier werden die Funktionen "put" und "get" durch die Basisklasse (Container) festgelegt und von der Flasche (Bottle) geerbt. Da es unsinnig wäre, automatisch alle Funktionen zu vererben, muss der Engine mitgeteilt werden, welche Funktionen die Flasche unterstützen soll. Dies geschieht mit dem Schlüsselwort "inherit", wobei dort die Parameter weggelassen werden.
Erweitertes Code-Beispiel:
Hier werden "put" und "get" zwar neu definiert, weswegen "inherit" wegfällt, der eigentliche Code ist aber weiterhin in der Basisklasse verstaut. Bei "open" und "close" wird jetzt eine Variable "$closed" auf 1 oder 0 gesetzt, welche dann bei "put" und "get" abgefragt wird.
Man beachte die Verwendung von "super", mit diesem Schlüsselwort werden die Funktionen der Basisklasse aufgerufen (ähnlich wie in Java). Dieses Vorgehen kann zu Konflikten führen, wenn mehrere Basisklassen existieren - mehr dazu im entsprechenden Abschnitt.
Code-Beispiel:
Der Holzstuhl verfügt nun über die "sit" und "burn" Funktionen der Basisklassen.
Im Beispiel wird die "cook" Funktion zuerst implizit und dann explizit eingebunden. Das heisst, im ersten Fall wurde nicht bestimmt, ob die Funktion der Soup oder Vegatable -Klasse gemeint ist. Yscript würde dann die Vererbungen der Reihe nach durchgehen und die Soup-Klasse wählen.
Im zweiten Fall jedoch wurde in Klammern explizit die Basisklasse genannt, daher gibt es keine Missverständnisse beim Einbinden. Diese "cook" Funktion wird von "Vegetable" abgeleitet.
Wie man sieht erbt hier jede Klasse nicht nur ihren direkten Vorgänger sondern auch fast alle vorhergehenden. Wenn Yscript jetzt eine Funktion sucht, wird es die Klassen in folgender Reihenfolge durchgehen: "Sportwagen, Auto, Fahrzeug, Auto, Fahrzeug".
Auch dieses Problem kann umgangen werden, indem die Basisklassen beim Ableiten explizit notiert werden.
In diesem Fall würde die Verwendung von "super" die falsche Funktion aufrufen, da "Soup" in der Vererbungsliste weiter vorne steht. Durch Angabe des Klassennamens vor dem Punkt kann dies jedoch verhindert werden.
Hier wird dieselbe Klasse zwei Mal definiert, wobei nur die letzte Definition im Spiel verwendet wird. Da es sich um eine Vererbung handelt, ist die Funktion "exit" auch in der zweiten Game-Klasse verfügbar.
Code-Beispiel:
// Basisklasse "Container"
class Container
{
function put( $item )
{
deb( "Item wurde reingelegt." );
}
function get( $item )
{
deb( "Item wurde entfernt." );
}
}
// Neue Klasse "Bottle"
class Bottle( Container )
{
inherit put;
inherit get;
function close()
{
deb( "Flasche ist jetzt zu." );
}
function open()
{
deb( "Flasche ist jetzt offen." );
}
}
Hier werden die Funktionen "put" und "get" durch die Basisklasse (Container) festgelegt und von der Flasche (Bottle) geerbt. Da es unsinnig wäre, automatisch alle Funktionen zu vererben, muss der Engine mitgeteilt werden, welche Funktionen die Flasche unterstützen soll. Dies geschieht mit dem Schlüsselwort "inherit", wobei dort die Parameter weggelassen werden.
Basisfunktionen erweitern
Nun kann die Flasche zwar geschlossen werden, der Inhalt ist aber nach wie vor zugänglich, da der Deckelstatus nirgends abgefragt wird. Anders gesagt: die "put" und "get" Funktionen, welche den Inhalt verwalten, haben überhaupt keinen Programmcode, der den Deckelstatus prüft. Man könnte jetzt die beiden Funktionen einfach neu schreiben (bzw. aus der Basisklasse kopieren und dann erweitern), aber das bläht den Code unnötig auf (in der Praxis sind die Funktionen meist "etwas" länger). Die elegante Lösung ist, nur den zusätzlichen Code zu schreiben, wie das folgende Beispiel zeigt:Erweitertes Code-Beispiel:
// Neue Klasse "Bottle" (Basisklasse "Container"
// siehe vorheriges Beispiel)
class Bottle( Container )
{
$closed = 0; // Lokale Variable initialisieren
function put( $item )
{
if( $closed == 1 ) return;
super( $item );
}
function get( $item )
{
if( $closed == 1 ) return;
super( $item );
}
function close()
{
$closed = 1;
deb( "Flasche ist jetzt zu." );
}
function open()
{
$closed = 0;
deb( "Flasche ist jetzt offen." );
}
}
Hier werden "put" und "get" zwar neu definiert, weswegen "inherit" wegfällt, der eigentliche Code ist aber weiterhin in der Basisklasse verstaut. Bei "open" und "close" wird jetzt eine Variable "$closed" auf 1 oder 0 gesetzt, welche dann bei "put" und "get" abgefragt wird.
Man beachte die Verwendung von "super", mit diesem Schlüsselwort werden die Funktionen der Basisklasse aufgerufen (ähnlich wie in Java). Dieses Vorgehen kann zu Konflikten führen, wenn mehrere Basisklassen existieren - mehr dazu im entsprechenden Abschnitt.
Multiple Vererbung
Manchmal ist es sinnvoll, mehrere Klassen in einer neuen Klasse zu vereinen. Als Beispiel dienen die Basisklassen "Chair" (Stuhl), wo der Spieler sich hinsetzen kann, und "Burnable" (brennbar), welche brennbare Objekte mit den notwendigen Funktionen ausstattet. Eine neue Klasse "Woodenchair" (Holzstuhl) soll nun beide Eigenschaften in sich vereinen.Code-Beispiel:
// Basisklasse "Chair"
class chair
{
function sit()
{
deb( "Du setzt dich hin." );
}
}
// Basisklasse "Burnable"
class Burnable
{
function burn()
{
deb( "Feuer! Es brennt!" );
}
}
// Neue Klasse "Woodenchair"
class Woodenchair( Chair, Burnable )
{
inherit sit;
inherit burn;
}
Der Holzstuhl verfügt nun über die "sit" und "burn" Funktionen der Basisklassen.
Problem: Verwechslungen
Was passiert, wenn zwei Basisklassen dieselben Funktionen haben? In diesem Fall geht Yscript die Vererbungsliste der Reihe nach durch und verwendet die jeweils erste vorkommende Funktion oder Klasse. Eine gezielte Ansteuerung der Klasse für diese Spezialfälle ist vorerst nicht geplant, man muss also die Liste der Klassennamen korrekt sortieren.class Soup()
{
function cook() { deb( "Mmh, Suppe." ); }
}
class Vegetable()
{
function cook() { deb( "Ah, Gemüse." ); }
}
class Implizit( Soup, Vegetable )
{
inherit cook; // Ausgabe: "Mmh, Suppe."
}
class Explizit( Soup, Vegetable )
{
inherit cook( Vegetable ); // Ausgabe: "Ah, Gemüse."
}
Im Beispiel wird die "cook" Funktion zuerst implizit und dann explizit eingebunden. Das heisst, im ersten Fall wurde nicht bestimmt, ob die Funktion der Soup oder Vegatable -Klasse gemeint ist. Yscript würde dann die Vererbungen der Reihe nach durchgehen und die Soup-Klasse wählen.
Im zweiten Fall jedoch wurde in Klammern explizit die Basisklasse genannt, daher gibt es keine Missverständnisse beim Einbinden. Diese "cook" Funktion wird von "Vegetable" abgeleitet.
Problem: Doppelvererbung
Was passiert, wenn man eine Klasse mehrfach vererbt, und in einer weiteren Klasse mehrere dieser Kinder erbt? Yscript lässt das zu und geht auch hier die Vererbungsliste der Reihe nach durch, was aber nicht immer zum gewünschten Ergebnis führt. Ein zusammengeschnittenes Beispiel:class Fahrzeug()
class Auto( Fahrzeug )
class Sportwagen( Auto, Fahrzeug )
class Ferrari( Sportwagen, Auto )
Wie man sieht erbt hier jede Klasse nicht nur ihren direkten Vorgänger sondern auch fast alle vorhergehenden. Wenn Yscript jetzt eine Funktion sucht, wird es die Klassen in folgender Reihenfolge durchgehen: "Sportwagen, Auto, Fahrzeug, Auto, Fahrzeug".
Auch dieses Problem kann umgangen werden, indem die Basisklassen beim Ableiten explizit notiert werden.
Explizite Klassenvererbung
In vielen Fällen will man die Funktionen der Basisklasse nicht nur übernehmen sondern auch ergänzen. Auch dabei kann es wieder zu Missverständnissen kommen, wenn mehrere Basisklassen dieselben Funktionen anbieten. Dieses Problem wird ebenfalls durch eine explizite Angabe des Klassennamens gelöst:class Gemüsesuppe( Soup, Vegetable )
{
function cook()
{
Vegetable.cook(); // Ausgabe: Ah, Gemüse.
}
}
In diesem Fall würde die Verwendung von "super" die falsche Funktion aufrufen, da "Soup" in der Vererbungsliste weiter vorne steht. Durch Angabe des Klassennamens vor dem Punkt kann dies jedoch verhindert werden.
Selbstvererbung
Durch Selbstvererbung ist es möglich, bereits kompilierte Klassen nachträglich zu erweitern. Auf diese Weise können z.B. Mods geschrieben werden, welche die ursprünglichen Skripts erweitern, ohne dass der Quelltext dieser Scripts bekannt ist. Das Prinzip folgt dann der gewöhnlichen Klassenvererbung wie oben beschrieben. Als Beispiel eine erweiterte Game-Klasse:// Originale game.dat
class Game
{
function init()
{
deb( "Willkommen im Spiel" );
}
function exit()
{
deb( "Spiel erfolgreich beendet" );
}
}
// Benutzer-Datei user.dat
class Game
{
function init()
{
deb( "Dieser Code wird zuerst ausgeführt" );
super(); // Die ursprüngliche init-Funktion aufrufen
}
}
Hier wird dieselbe Klasse zwei Mal definiert, wobei nur die letzte Definition im Spiel verwendet wird. Da es sich um eine Vererbung handelt, ist die Funktion "exit" auch in der zweiten Game-Klasse verfügbar.