In Verse kannst du eine Klasse erstellen, die die Definition einer anderen Klasse erweitert, indem du die Felder und Methoden der anderen Klasse hinzufügst oder änderst. Dies wird oft als Subclassing oder Vererbung bezeichnet, da eine Klasse Definitionen von der anderen Klasse erbt.
Betrachten wir das Klassengestalter-Gerät als Beispiel für Subklassen. Mit dem Gerät Klassen-Designer kannst du Charakterklassen für Spielercharaktere anlegen, mit denen du die für eine Charakterklasse spezifischen Attribute und Vorräte festlegen kannst, z. B. für einen Tank- oder DPS-Charakter (Damage per second).
DPS-Charakterklasse erstellt mit dem Klassengestalter-Gerät | Tank-Charakterklasse erstellt mit dem Klassengestalter-Gerät |
In Verse könntest du eine Tank-Klasse und eine DPS-Klasse wie folgt erstellen:
tank := class():
StartingShields : int
MaxShields : int
AllowOvershield : logic
DamageReduction : int
dps := class():
StartingShields : int
MaxShields : int
AllowOvershield : logic
Da einige der Felder in den beiden Klassen identisch sind, kannst du Duplizierungen mit einer Superklasse reduzieren, die die gemeinsamen Eigenschaften und Verhaltensweisen der Klassen enthält. Nennen wir diese Superklasse player_character und machen tank und dps zu Subklassen von player_character:
player_character := class():
StartingShields : int
MaxShields : int
AllowOvershield : logic
dps := class(player_character):
MovementMultiplier : float
tank := class(player_character):
DamageReduction : intDa die Klassen tank und dps Subklassen von player_character sind, erben sie automatisch die Felder und Methoden der Klasse player_character, sodass du nur angeben musst, was sich in dieser Klasse von der Superklasse unterscheidet.
Beispielsweise fügt die Klasse dps nur das Feld Movement Multiplier hinzu, und die Klasse tank fügt nur das Feld DamageReduction hinzu. Diese Konfiguration ist nützlich, wenn du das gemeinsame Verhalten der beiden Klassen später änderst, da du es nur in der Superklasse ändern musst.
Mit Verse kannst du weitere Änderungen hinzufügen, um die Klassen „tank“ und „dps“ zu unterscheiden, indem den Subklassen Methoden hinzufügst.
Ein nützlicher Effekt der Subklassenbildung ist, dass du die Beziehung zwischen einer Superklasse und ihren Subklassen nutzen kannst. Aufgrund der Vererbung ist eine Instanz von tank ein spezialisierter player_character, und eine Instanz von dps ist ein spezialisierter player_character, was als eine „is-a“-Beziehung bezeichnet wird. Da tank und dps beide Subklassen derselben Superklasse sind und von ihrer gemeinsamen Superklasse abweichen, hat tank keine Beziehung mit dps.
Override-Bezeichner
Um Instanzen von Klassen mit anfänglichen Werten zu erstellen, ist es üblich, eine Funktion zu haben, die die Instanzen erzeugt. Beispiel:
CreateDPSPlayerCharacter() : dps =
return dps{StartingShields := 0, MaxShields := 0, AllowOvershield := false, MovementMultiplier := 1.9}
CreateTankPlayerCharacter() : tank =
return tank{StartingShields := 100, MaxShields := 200, AllowOvershield := true, DamageReduction := 50}Die Funktionen CreateTankPlayerCharacter() und CreateDPSPlayerCharacter() erstellen die Instanzen mit den entsprechenden Anfangswerten. Alternativ kannst die Felder der Superklasse überschreiben (override) und ihnen Anfangswerte zuweisen, sodass du bei der Erstellung einer Instanz nicht so viele Anfangswerte angeben musst.
Die Klasse „tank“ aus dem vorigen Abschnitt könnte mit Overrides für die Felder zum Beispiel so aussehen:
tank := class(player_character):
StartingShields<override> : int = 100
MaxShields<override> : int = 200
AllowOvershield<override> : logic = true
DamageReduction : int = 50
CreateTankPlayerCharacter() : tank =
return tank{}Du kannst auch Methoden in der Subklasse überschreiben, was bedeutet, dass du die überschreibende Methode überall dort verwenden kannst, wo die überschriebene Methode verwendet werden kann. Dies bedeutet:
Die Methode muss mindestens jedes Argument akzeptieren, das von der überschriebenen Methode akzeptiert wird, der Parametertyp muss also ein Supertyp des Parametertyps der überschriebenen Funktion sein.
Die Methode darf keinen Wert zurückgeben, den die überschriebene Methode nicht haben kann, also muss der Rückgabetyp ein Subtyp des Rückgabetyps der überschriebenen Methode sein.
Die Methode darf nicht mehr Effekte haben als die überschriebene Methode, daher muss der Effekt-Bezeichner ein Subtyp des Effekt-Bezeichners der überschriebenen Methode sein.
Super
Ähnlich wie bei Self kannst du mit (super:) auf die Implementierungen von Feldern und Methoden der Superklasse zugreifen. Um (super:) verwenden zu können, muss das Feld oder die Methode in der Superklassendefinition implementiert werden.
pet := class():
Sound : string
Speak() : void =
Log(Sound)
cat := class(pet):
Sound<override> : string = "Meow"
Speak<override>() : void =
Blockausdrücke in einem Subclass-Körper
Alle block-Ausdrücke, die sich im Körper einer Subklasse befinden, werden nach den block-Ausdrücken ausgeführt, die im Körper der Superklasse angegeben sind. Zum Beispiel wird im folgenden Code, wenn die Instanz der Klasse cat mit dem Namen MrSnuffles erstellt wird, zuerst Speak() und dann Purr() ausgeführt.
pet := class():
Speak() : void =
...
block:
Speak()
cat := class(pet):
Purr() : void =
...
Abstract-Bezeichner
Wenn eine Klasse oder eine Klassenmethode über den Abstract-Bezeichner verfügt, kannst du keine Instanz der Klasse erstellen. Abstrakte Klassen sind dazu konzipiert, als Oberklasse mit teilweiser Implementierung oder als gemeinsames Interface verwendet zu werden. Dies ist nützlich, wenn es keinen Sinn macht, Instanzen einer Superklasse zu haben, du aber keine Eigenschaften und Verhaltensweisen in ähnlichen Klassen duplizieren willst.
Da pet im folgenden Beispiel ein abstraktes Konzept ist, ist eine Instanz der Klasse „pet“ nicht spezifisch genug, aber ein pet „cat“ oder „dog“ ist sinnvoll, sodass diese Subklassen nicht als abstrakt gekennzeichnet sind.
pet := class<abstract>():
Speak() : void
cat := class(pet):
Speak() : void =
...
dog := class(pet):
Speak() : void =
...