RGSS/Tutorials/Rubykurs 2 - OOP Teil 1
| Rubykurs | |
|---|---|
| Autor | Kai D |
| Thematik | Ruby |
| Vorraussetzungen | Programme: RPG-Maker XP Fähigkeiten: Grundlagen des Eventscripting im RPG-Maker XP |
| Andere Teile | |
Nachdem es im vorherigen Kapitel schon oft genug angesprochen wurde, wollen wir uns nun dem Objektorientierten Programmieren (kurz: OOP) zuwenden. Dies wird weit weniger kompliziert als es klingt, denn OOP ist sehr einfach zu erlernen. Schwieriger wird es hingegen, dieses Konzept auch konsequent anzuwenden. Denn der Maker selbst verhält sich alles andere als objektorientiert, so dass für erfahrene Makerer in Ruby eine Umstellung erfolgen muss.
Inhaltsverzeichnis
Einstieg
Die erste Frage, die man sich bei dem Thema "Objektorientiertes Programmieren" stellen muss, ist natürlich: Was sind eigentlich Objekte? Ich möchte diese Frage mit einer Gegenfrage zu beantworten versuchen: Was ist auf diesem Bild abgebildet?
Einige werden sicherlich "Baum" geantwortet haben. Andere vielleicht auch "Tentakelwesen", meine Zeichenkünste lassen da keine eindeutige Interpretation zu. Falsch liegt ihr damit aber trotzdem. Auf dem Bild ist nämlich eine geschlungene Linie gezeichnet.
Har! Reingelegt! Wer darauf reingefallen ist, möge nicht weiter enttäuscht sein. Das wir in diesem Bild sofort einen Baum erkennen ist natürlich und für den Alltag ungemein nützlich. Wenn man so durch eine Allee spaziert, fragt man sich doch irgendwie, weshalb der Mensch jeden dieser großen Objekte sofort als Baum erkennt. Ich meine, die sehen doch alle unterschiedlich aus. Mancher mit mehr, andere mit weniger Blättern. Einer mit weißer Rinde, der nächste mit dunkelbrauner. Selbst völlig neue und exotische Arten werden sofort als Baum erkannt, wenn sie denn einer sind. Wir sind also in der Lage völlig verschiedene Dinge in eine gemeinsame Kategorie unterzubringen. Selbst abstrakte Dinge wie diese Zeichnung dort oben, die überhaupt nichts mit unseren Erfahrungen von Bäumen zu tun hat wird als solcher erkannt. Man nennt diese Fähigkeit abstraktes Denken und der Mensch beherrscht sie ausgezeichnet.
Schon der griechische Philosoph Platon befasste sich mit dieser Problematik. Er stellte daraufhin seine Theorie von den Ideen auf. Jeder Mensch hat Ideen, von denen er reale Objekte ableiten kann. So hat der Mensch im Kopf eine Idee von einem Pferd. In dieser Idee ist ein allgemeines, abstraktes Pferd beschrieben. Zum Beispiel: Vier Beine, Mähne, Hufen, längliches Maul. Jedes reale Objekt auf der Welt, dass dieser Idee entspricht, wird von uns als Pferd anerkannt. Ebenso können wir uns Kraft unserer Fantasie verschiedene Pferde aus dieser Idee ableiten - darunter auch einige, die es so gar nicht in der Welt gibt. Man denke an die griechische Mythologie mit ihren Pegasus und anderen Mythengestalten.
Kommen wir zurück zu Ruby. Der Computer ist leider nicht des abstrakten Denkens fähig, also müssen wir es ihm beibringen. Diese Ideen, von denen Platon sprach, werden in objektorientierten Sprachen als Klassen bezeichnet. Eine Klasse ist also eine allgemeine, abstrakte Idee. Ihre Realisierung bezeichnet man Objekt. Zum Beispiel gibt es eine allgemeine Idee für ein Wort. Sie könnte lauten: Besteht aus Schriftzeichen, kann gelesen werden etc. In Ruby gibt es eine allgemeine Klasse für Wörter. Wir haben sie bereits kennengelernt: String.
Ebenso gibt es auch eine allgemeine Klasse für ganzstellige Zahlen: Integer. Sowie eine allgemeine Klasse für Reihungen: Array. All diese Datentypen, wie wir sie zuvor genannt haben, sind Klassen und damit Ideen von Objekten. Die Klasse String bezeichnet also, wie eine Zeichenkette auszusehen hat. "Hallo Welt" ist ein real existierendes Objekt, dass dieser Klasse abgeleitet wurde. Man könnte Unmengen an verschiedenen Zeichenkombinationen aus dieser Klasse ableiten. Die Klasse String beschreibt, was alle diese Wörter gemeinsam haben.
Was haben sie denn gemeinsam? Was beschreibt so eine Klasse überhaupt? Kommen wir also zurück zu unserem Baum und stellen wir uns die Frage: Was ist ein Baum? Ich denke die meisten werden erst einmal anfangen, die Form eines Baumes zu beschreiben, denn daran haben sie ja schließlich auch mein Bild erkannt. Ein Baum besteht aus Wurzel, Stamm und Krone. Huch! Das sind ja auch Objekte! Damit hätten wir schon einmal eine wichtige Entdeckung gemacht: Objekte können aus anderen Objekten bestehen!
Fragen wir weiter: Was ist eine Krone? Hm, ein Geflecht aus Ästen, Zweigen und Blättern. Was ist ein Ast? Ein hölzernes Material. Was ist Holz? Eine braune, raue Masse. Was ist Braun? Geben wir es auf, spätestens hier werden wir nicht weiterkommen, denn wie will man einen Sinneseindruck beschreiben? Auch in Ruby können Objekte aus anderen Objekten bestehen. Ein Array besteht aus den vielen Elementen, die man ihm zugewiesen hat. Ein String besteht aus vielen Schriftzeichen. Ein Range besteht aus einem Anfangswert und einem Endwert. Diese Elemente eines Objektes bezeichnet man als Instanzvariablen. So ist die Anzahl der Blätter eine Instanzvariable aller Baumobjekte.
Gehen wir mal von den Beispielen aus der Natur weg und kommen zu den menschlichen Errungenschaften: Was ist ein Tisch? Will man jetzt die äußere Form beschreiben, so wird man kläglich scheitern. Früher mochte die Beschreibung "Rechteckige Platte, vier Tischbeine" ja noch zugetroffen haben, doch die neusten Designermodelle haben ja jede erdenkliche Form. Ist ein dreibeiniger Tisch mit einer dreieckigen Platte etwa kein Tisch mehr? Mit Verlaub, es ist ein Tisch! aber was macht ihn zum Tisch? Nicht das Aussehen, sondern die Funktion! Ich kann auf ihm essen, lesen, ich kann andere Objekte auf ihn abstellen. Viel wichtiger als die Form ist die Funktion eines Objektes. Ein Baum betreibt Photosynthese, er lebt, er atmet, er wächst, er raschelt mit den Blättern.
Wir kennen bereits das Rubyäquivalent zu den Funktionen realer Objekte der Natur. Es sind Methoden. Jede Klasse beschreibt welche Methoden ihre abgeleiteten Objekte haben. Zwei Objekte der gleichen Klasse unterscheiden sich zwar darin, aus welchen Objekten sie bestehen (der Array [3,1,2] besteht zum Beispiel aus dem Objekt 3. Der Array ["hallo", 4, 1..2] besteht hingegen aus dem Objekt "hallo". Dennoch gehören sie der gleichen Klasse an.), aber sie haben stets die selben Methoden. Jeder Array beherrscht genau die gleichen Methoden wie ein anderer Array.
Die meisten modernen, hohen Programmiersprachen bieten objektorientierte Programmierweise an, aber Ruby ist durch und durch objektorientiert, dass heißt: In Ruby besteht so ziemlich alles aus Objekten. Auch Klassen und Methoden sind beispielsweise Objekte. Will man die Grundsätze des OOPs kurz zusammenfassen, so käme man auf folgende Regeln:
- Alles ist ein Objekt
- Jedes Objekt hat eine Klasse
- Jede Klasse besitzt eine Superklasse (Ausnahme: die Klasse Object)
- Klassen besitzen Methoden, die ihre Fähigkeiten beschreiben, und Instanzvariablen, die ihre Eigenschaften beschreiben
Was diese Grundsätze konkret heißen, wird zur rechten Zeit noch erklärt.
Beschreibe folgende Objekte:
- Vampires Dawn
- Roter Drache
- Heiltrank
Der Vampir Valnar und der Kämpfer Grandy gehören beide der Klasse Held an. Ist es möglich, dass Valnar die Methode verwandlung_in_fledermaus() besitzt? Begründe deine Entscheidung.
Klassen und Objekte
Nun haben wir lange genug über Klassen und Objekte philosophiert. Lasst uns endlich eine eigene Klasse schreiben!
class Klassenname def methodenname #Methodeninhalt end def anderer_methodenname #Methodeninhalt end end
Hier haben wir das Muster einer typischen Klasse. Sie beginnt mit dem Schlüsselwort class und endet mit dem Schlüsselwort end. Dazwischen stehen ihre Methoden. Schreiben wir einmal eine Klasse Held:
class Held def rette_welt print("Ich werde die Welt vor allem Übel befreien!") end def vorstellung(name) print("Hallo Herr ", name, ". Ich bin ein großer Held!") end def drachentoeten(anzahl) until anzahl <= 0 print("Was? Noch ", anzahl , " Drachen? Dich werde ich auch noch niederstrecken!") anzahl -= 1 end print("Die Drachenjagd war erfolgreich.") end end
Wie bereits im Einstieg erwähnt wurde, stellt diese Klasse nur eine allgemeine Idee eines Helden dar. Nun müssen wir aus ihr einen realen Helden, also ein Objekt, ableiten. Das ist vergleichbar mit der Herstellung eines Autos. Zuerst wird ein Bauplan eines Autos entworfen, danach wird in der Fabrik ein richtiges Auto nach diesem Bauplan gefertigt. Dieses aus einer Klasse abgeleitete Objekt bezeichnet man als Instanz der Klasse.
Eröffnen wir also eine Heldenfabrik und erzeugen einen neuen Helden!
alex = Held.new #alex ist eine Instanz von Held
Und das war auch schon alles. Man erstellt eine Instanz, in dem man die Methode new der jeweiligen Klasse ausführt. Damit man mit dem erzeugten Objekt auch etwas anfangen kann, weist man ihm üblicherweise noch eine Variable zu. Die heißt in diesem Beispiel alex. Ich hätte ihr aber auch jeden anderen Namen geben können. Nun haben wir unser Objekt und es ist in der Lage die Methoden, die wir in seine Klasse geschrieben haben, auszuführen.
alex.rette_welt #=>Ich werde die Welt vor allem Übel befreien! alex.vorstellung("Wahnfried") #=>Hallo Herr Wahnfried. Ich bin ein großer Held! alex.drachentoeten(3) #=> Was? Noch 3 Drachen? Dich werde ich auch noch niederstrecken! #=> Was? Noch 2 Drachen? Dich werde ich auch noch niederstrecken! #=> Was? Noch 1 Drachen? Dich werde ich auch noch niederstrecken! #=> Die Drachenjagd war erfolgreich
Na das klappt doch wunderbar. Schön wäre es aber, wenn der Held, sobald er erstellt wird, erst einmal einen 08/15 Spruch aufsagt, ohne dass man ihn dazu zwingen muss. Es muss doch irgendwie möglich sein, dass ein Objekt nach seiner Erstellung sofort etwas besonderes macht. In Ruby gibt es dafür die initialize Methode. initialize ist englisch und heißt übersetzt initialisieren/voreinstellen. Sie kann in jede Klasse definiert werden.
class Held def initialize print("Mein Dorf wurde niedergebrannt! Also werde ich Söldner, finde ein magisches Schwert und rette die Welt.") end end lazalantin = Held.new #=> Mein Dorf wurde niedergebrannt! Also werde ich Söldner, #=> finde ein magisches Schwert und rette die Welt.
Will man von einer Klasse verschiedene Spezialisierungen haben, so macht man sich das Prinzip der Vererbung zu Nutze. Stellen wir uns vor, wir benötigen für unser Spiel eine Klasse Auto und eine Klasse Fahrrad. Beide Klassen haben viele Gemeinsamkeiten: Beide haben Räder, beide werden von einem Menschen gefahren, beide können sich fortbewegen etc. Da wäre es doch schade, wenn wir diese Fähigkeiten, die eigentlich beide Klassen haben, doppelt aufschreiben müssten.
Stattdessen schreiben wir eine Superklasse namens Fahrzeug, welche diese allgemeinen Fähigkeiten beherrscht. Auto und Fahrrad sind von nun an die Subklassen von Fahrzeug und beherrschen so automatisch alles, was auch die Superklasse beherrscht - nur eben noch mehr.
Wie funktioniert die Vererbung? Wenn eine Klasse eine Superklasse bekommt, so erbt sie von dieser alle Eigenschaften und Methoden.
class Fahrzeug def fahre(ziel) print("Ich fahre nach ", ziel) end def bremse print("Ich halte an!") end end class Fahrrad < Fahrzeug def raeder 2 end def klingeln print("*kling*") end end mein_fahrrad = Fahrrad.new mein_fahrrad.fahre("Hause") #=>Ich fahre nach Hause mein_fahrrad.bremse #=>Ich halte an! mein_fahrrad.klingeln #=>*kling*
In diesem Beispiel haben wir die Klasse Fahrzeug und die Subklasse Fahrrad erstellt. Um Ruby zu erklären, dass Fahrrad eine Subklasse von Fahrzeug ist, schreiben wir class Subklasse < Superklasse
Im Code ist ersichtlich, dass die Instanz von Fahrrad nicht nur ihre eigenen Methoden, sondern auch noch die ihrer Superklasse ausführen kann.
class Auto < Fahrzeug def bremse print("Ich trete auf das Bremspedal und das Auto hält an!") end end fahrrad = Fahrrad.new fahrrad.bremse #=>Ich halte an! auto = Auto.new auto.bremse #=>Ich trete auf das Bremspedal und das Auto hält an!
Wenn die Subklasse eine Methode enthält, die auch die Superklasse besitzt, so wird beim Aufruf immer nur die Methode der Subklasse ausgeführt.
Manchmal wollen wir aber sowohl die Methode der Superklasse aufrufen, als auch der Methode etwas eigenes hinzufügen. Dies lässt sich mit dem Befehl super() erledigen.
class Auto < Fahrzeug def fahre(ziel, fuehrerschein) if fuehrerschein == true then super(ziel) else print("Du kannst nicht ohne Führerschein mit dem Auto nach ", ziel, " fahren!") end end end omas_auto = Auto.new omas_auto.fahre("Düsterburg", false) #=>Du kannst nicht ohne Führerschein #=> mit dem Auto nach Düsterburg fahren! omas_auto.fahre("Düsterburg", true) #=> Ich fahre nach Düsterburg
super(parameter1, parameter2, ...) ruft die gleichnamige Methode der Superklasse mit den angegebenen Parametern auf. In diesem Beispiel wird abgefragt, ob der Autobesitzer einen Führerschein besitzt. Ist dies der Fall, so wird die Methode fahre(ziel) der Superklasse Fahrzeug aufgerufen. Hat man jedoch keinen Führerschein, so erhält man eine Mahnrede.
Das praktische daran: Wenn man nun die Methode der Superklasse ändert, wirkt sich dies auch auf die Subklassen auf, welche die gleichnamige Methode mit super aufrufen (oder welche keine gleichnamige Methode definiert haben).
class Fahrzeug def fahre(ziel) print("Dann wollen wir mal nach ", ziel, " aufbrechen!") end end opas_auto = Auto.new opas_auto.fahre("Königsberg", true) #=> Dann wollen wir mal nach Königsberg aufbrechen!
Wollen wir die Methode der Superklasse mit den gleichen Parametern aufrufen, wie die Methode der Subklasse, dann brauchen wir nur super hinzuschreiben (also ohne Klammern). Das ist allerdings etwas tückisch, weshalb ich dazu raten würde, hinter das super grundsätzlich Klammern zu setzen und die Parameter explizit hinzuschreiben.
class Motorisiertes_Fahrzeug < Fahrzeug def fahre(ziel) print("Erst mal Motor einschalten") # wir lassen die Klammern weg super end end auto = Motorisiertes_Fahrzeug.new auto.fahre("Königsberg") #=> Erst mal Motor einschalten. Dann wollen wir mal nach Königsberg aufbrechen!
Ist das selbe wie:
class Motorisiertes_Fahrzeug < Fahrzeug def fahre(ziel) print("Erst mal Motor einschalten") # wir schreiben die Klammern mitsamt Parameter hin super(ziel) end end auto = Motorisiertes_Fahrzeug.new auto.fahre("Königsberg") #=> Erst mal Motor einschalten. Dann wollen wir mal nach Königsberg aufbrechen!
Superklassen sind immer dann nützlich, wenn mehrere Klassen gemeinsame Eigenschaften haben. Dann ist es sinnvoll eine gemeinsame Superklasse zu schreiben, welche diese Eigenschaft inne hat. Jede Klasse kann nur eine einzige Superklasse, dafür aber beliebig viele Subklassen haben. Die Superklassen selbst dürfen wiederum ebenfalls eine eigene Superklasse haben - eine Kette, die sich beliebig lang fortsetzen lässt. Übrigens hat jedes Objekt eine Superklasse, selbst dann, wenn wir keine angeben. In diesem Fall ist nämlich automatisch die Klasse Object die Superklasse. Object ist auch die einzige Klasse in Ruby, welche keine eigene Superklasse hat. Die Konsequenz daraus ist, dass alle Methoden, die in Object definiert sind, von jedem Objekt ausgeführt werden können (da ja jedes Objekt von der Klasse Object abstammt). Methoden wie print() sind also (indirekt) Methoden der Klasse Object und werden von ihr an alle anderen Klassen weiter vererbt.
Später werden noch weitere Möglichkeiten genannt, wie man verschiedenen Klassen gleiche Methoden zusteuern kann (nämlich durch sogenannte Mixins).
Gegeben sind 10 Begriffe, von denen 7 fiktive Klassen darstellen. Suche zuerst die Klassen heraus und überlege dann, welche Klasse die Superklasse einer anderen ist. Jede Klasse muss am Ende eine Superklasse haben. Auch hier handelt es sich um eine reine Verständnisfrage. Es geht nicht darum, ob diese Klassen und Objekte wirklich existieren. Stellt euch vor, sie würden existieren, und überlegt euch in welcher Reihenfolge die Vererbung abläuft.
- Inventarobjekt, Questitem, Waffe, Valnars Langschwert, Heilitem,
Schreibe eine Klasse Wuerfel, mit den Methoden einmal_werfen() und mehrmals_werfen(n). Die Methode einmal_werfen() gibt eine zufällige Zahl von 1 bis 6 zurück. Die Methode mehrmals_werfen(n) gibt einen Array mit n Elementen zurück, wobei jedes Element durch einmal_werfen ermittelt wird.
Tipp: rand(n) hat als Rückgabewert eine zufällige Zahl von 0 bis n-1.
Schreibe eine Subklasse Gezinkter_Wuerfel < Wuerfel. Bei einem gezinkten Würfel ist das Würfelergebnis bei 50% der Fälle genauso wie bei einem gewöhnlichen Würfel. In den anderen 50% der Fällen ist das Würfelergebnis eine 6.
Schreibe eine Methode verteilung() für die Klasse Wuerfel. Die Methode soll einen Array mit 6 Elementen zurückgeben, wobei das (n-1)-te Element angibt, zu wie viel Prozent dieser Würfelwert geworfen wird. Die Prozentzahlen sollen statistisch ermittelt werden. Das heißt: Der Würfel wird 1000 Mal geworfen, danach wird gezählt welcher Würfelwert wie oft geworfen wurde und das Verhältnis gebildet. Vergleiche das Ergebnis eines Würfels mit dem eines gezinkten Würfels.
Variablen
Wir haben gelernt, wie man Klassen und ihre Methoden erzeugt. Zuvor wurde aber gesagt, dass Objekte nicht nur Zugriff auf verschiedene Methoden haben, sondern auch aus anderen Objekten bestehen können. Um dies zu realisieren, müssen wir einen kurzen Ausflug zum Thema Variablen machen.
Variablen sind, wie im ersten Kurs schon einmal erwähnt wurde, die Namen/Adressen/Signaturen von Objekten. Ein Objekt ohne Variable ist nutzlos und wird gelöscht. Daher ist es wichtig, dass jedes Objekt einer Variable zugeordnet wird. Verliert ein Objekt seine Zuweisung, so landet es in der Müllsammlung Rubys, dem sogenannten GC (Garbage Collector).
Man kann das Zusammenspiel von Objekten und Variablen auch bildlich darstellen:
meine_variable = "Ein Objekt" #Ein Objekt der Klasse String wird erzeugt. #Ihm wird die Variable meine_variable zugeordnet meine_variable = nil #Nun wird der Variable ein neues Objekt, nil, zugeordnet #Da eine Variable nie auf mehr Objekte zur gleichen Zeit verweisen darf, verliert der String seine Variable #Der String landet nun in der Müllsammlung und wird beizeiten gelöscht
In diesem Code wird zum Beispiel eine Zuweisung geändert. Eine Variable hat erst die Zuweisung zum Objekt "Ein Objekt". Danach wird diese Zuweisung gelöst. Das Seil reißt und hält stattdessen das Objekt nil. Das Objekt "Ein Objekt" wird von keinem Seil mehr gehalten, fällt in den Kochtopf und wird gelöscht. Mit diesem Wissen ist man also in der Lage, Objekte zu löschen, wenn man sie nicht mehr braucht.
Es gibt aber auch viel effektivere Methoden, in dem man Objekte von anderen Objekten abhängig macht. Wird das eine Objekt gelöscht, so werden automatisch auch die anderen Objekte gelöscht. Es gibt auch Variablen, die niemals ihre Zuweisung ändern. Kurzum: Es gibt verschiedene Variablentypen, mit denen man den Zugriff und die Lebensdauer eines Objektes bestimmen kann.
lokale Variablen
Die ersten Variablen, die wir im Tutorial behandelt haben, waren lokale Variablen.
Alle Variablen, die mit einem Kleinbuchstaben oder mit _ beginnen, sind lokale Variablen!
Eine lokale Variable ist immer an einen Block gebunden. Das heißt, sie erhält nur so lange ihre Zuweisung, wie der Block ausgeführt wird. Sobald eine Block zuende ausgeführt wurde, verliert die lokale Variable ihr zugewiesenes Objekt. In Ruby gibt es verschiedene Blöcke. Das typischste Beispiel ist ein Methodenblock. Eine lokale Variable innerhalb eines Methodenblockes kann nicht von außerhalb verwendet werden.
Beispiel:
class Held def rette_prinzessin prinzessin = "Heide" _DerHeld = "Alex" print(_DerHeld, " rettet die Prinzessin ", prinzessin) end end mein_held = Held.new mein_held.rette_prinzessin #=>Alex rettet die Prinzessin Heide print(prinzessin, "wurde gerettet!")#=>No method error!
Sowohl prinzessin, als auch _DerHeld sind lokale Variablen. Sie gelten nur innerhalb der Methode rette_prinzessin. Dort werden ihnen zwei Strings zugewiesen. Versucht man die Variablen außerhalb der Methode aufzurufen, so stürzt das Projekt ab, da kein Objekt gefunden werden kann, dass dieser Variable zugeordnet ist.
Aber selbst wenn eine Methode noch nicht zu Ende ausgeführt wurde, kann eine lokale Variable nicht von außerhalb der Methode aufgerufen werden.
class Held def kuesse_prinzessin prinzessin = "Heide" _DerHeld = "Alex" romantische_szene print("Die Wege von ", prinzessin, " und ", _DerHeld, " trennen sich.") #=>Die Wege von Heide und Alex trennen sich. end def romantische_szene print(prinzessin, " wird von ", _DerHeld, " geküsst.") #=>No method error! end end
In diesem Beispiel versucht die Methode romantische_szene die lokalen Variablen der Methode kuesse_prinzessin zu benutzen. Aber das funktioniert nicht, wie der Programmabsturz demonstrieren sollte. Lokale Variablen sind also fest an ihre Methoden gebunden. Sie können nur dort aufgerufen werden und verschwinden, sobald die Methode endet.
Parameter sind übrigens ebenfalls lokale Variablen. Übergibt man ein Objekt einer Methode in Form eines Parameters, so erhält das Objekt eine neue lokale Variable, welche aber nur in dieser Methode gültig ist. Parameter sind allerdings eine Möglichkeit, anderen Methoden lokale Variablen der eigenen Methode zu übertragen.
class Held def kuesse_prinzessin prinzessin = "Heide" _DerHeld = "Alex" romantische_szene(prinzessin, _DerHeld) print("Die Wege von ", prinzessin, " und ", _DerHeld, " trennen sich.") #=>Die Wege von Heide und Alex trennen sich. end def romantische_szene(dame, herr) print(dame, " wird von ", herr, " geküsst.") #=>Heide wird von Alex geküsst. end end
Diesmal dürfte es nicht zu einem Absturz kommen. Die Methode kuesse_prinzessin erzeugt zwei Objekte, "Heide" und "Alex", und weist ihnen die lokalen Variablen prinzessin und _DerHeld zu. Wir haben gelernt, dass ein Objekt beliebig viele Variablen haben kann. Durch den Aufruf der Methode romantische_szene mit den beiden Objekten als Parameter, erhält "Heide" eine weitere Variable namens dame, und "Alex" erhält eine zweite Variable namens herr. Nun wird es knifflig. Alex ist ein Objekt mit zwei lokalen Variablen. _DerHeld ist eine lokale Variable der Methode kuesse_prinzessin. Sie kann also nur innerhalb dieser Methode benutzt werden. herr ist hingegen eine lokale Variable der Methode romantische_szene und sie kann nur dort verwendet werden. Dennoch zeigen beide Variablen auf das gleiche Objekt.
Wird die romantische_szene ausgeführt, so verliert "Alex" seine Variable herr wieder. Da er jedoch noch eine Variable _DerHeld hat, wird das Objekt noch nicht in die Müllsammlung geschickt. Erst wenn nach Ausführen der Methode kuesse_prinzessin das Objekt auch seine zweite Variable verliert, wird es nutzlos und landet in dem GarbageCollector.
Diese Variablenart wird für Zwischenergebnisse und Parameter von Methoden genutzt. Sie ist allerdings nicht in der Lage, Objekte langfristig zu erhalten. Eine Ausnahme stellen lokale Variablen dar, die außerhalb einer Methode erzeugt werden. Allerdings sollte so etwas, abgesehen von Test- und Lernprogrammen, möglichst vermieden werden. Obwohl nur der Anfangsbuchstabe einer lokalen Variable in Kleinschrift oder _ sein muss, sollte man sich angewöhnen lokale Variablen generell klein zu schreiben. _DerHeld ist also eine eher unschöne Schreibweise. der_held wäre die bessere Wahl gewesen.
Globale Variablen
Im Gegensatz zu den lokalen Variablen sind globale Variablen nicht an eine Methode, noch an irgendetwas anderem gebunden. Sie verlieren ihre Zuweisung nur nach Beenden des Programms. Außerdem kann man von überall aus auf sie zugreifen.
Eine globale Variable beginnt mit dem Dollarzeichen $
Im Gegensatz zu lokalen Variablen stürzt das Programm nicht ab, wenn man eine globale Variable aufruft, ohne ihr zuvor ein Objekt zuzuweisen. Eine globale Variable, der noch kein Objekt zugewiesen wurde, ist automatisch nil.
def schreibe_globale_variable $firlefanz = "Hallo Welt " end def wiederhole_globale_variable print($firlefanz, $firlefanz, $firlefanz) end print($firlefanz) #=> nil schreibe_globale_variable print($firlefanz) #=> Hallo Welt wiederhole_globale_variable #=> Hallo Welt Hallo Welt Hallo Welt
Wie man sieht kann man von verschiedenen Methoden aus auf die gleiche Variable zugreifen.
Globale Variablen sollten höchst sparsam genutzt werden. Ein Objekt, auf das eine globale Variable zeigt, wird erst dann in die Müllsammlung geschickt, wenn man der Variable ein neues Objekt zuweist. Daher sollte man globale Variablen nur für Objekte nutzen, die man das ganze Programm über braucht. Beispiele für globale Variablen wären die Variablen des Makers $game_variables. Sie dürfen nie gelöscht werden, schließlich sollen sie das ganze Spiel über unverändert bleiben. Außerdem werden sie an den verschiedensten Stellen des Spiels gebraucht, so dass eine globale Variable sich hier anbietet.
Instanzvariablen
Kommen wir endlich zurück zu unseren Objekten. Es hieß, dass ein Objekt aus anderen Objekten bestehen kann. Dafür sind Instanzvariablen zuständig. Wie zuvor schon erwähnt, ist eine Instanz das abgeleitete Objekt einer Klasse. [4,2.5,"bla"] ist eine Instanz der Klasse Array. 8.3 ist eine Instanz der Klasse Float. Instanzvariablen sind Variablen, die an eine Instanz gebunden sind. Sie dürfen also nur innerhalb einer Instanz aufgerufen werden und werden gelöscht, sobald eine Instanz in die Müllsammlung gerät. Sie sind also von ihrer Instanz abhängig.
Instanzvariablen beginnen mit dem Zeichen @
Instanzvariablen verweisen auf das Objekt nil, solange ihnen kein anderer Wert zugewiesen wird. Üblicherweise weist man Instanzvariablen in der initialize Methode ihr Objekt zu.
class Held def initialize(name) @name = name end def vorstellen print("Ich heiße ", @name) end end alex = Held.new("Alex") alex.vorstellen #=>Ich heiße Alex
In diesem Beispiel wird beim Erstellen der Instanz alex der initialize Methode der Parameter "Alex" zugewiesen. Die lokale Variable name verweist also auf das Objekt "Alex". Schön und gut, nur wollen wir, dass der Held sich seinen eigenen Namen merkt. Daher weisen wir dem Objekt von name noch eine Instanzvariable @name zu. Der Name des Helden, "Alex", ist nun an das Objekt von alex gebunden. Alle Methoden dieser Instanz dürfen darauf zugreifen.
Das tolle an Instanzvariablen ist, dass die Instanzvariablen verschiedener Instanzen auch auf verschiedene Objekte verweisen.
held1 = Held.new("Alex") held2 = Held.new("Valnar") held3 = Held.new("Grandy")
Nun haben wir drei Objekte, die alle eine Instanzvariable @name haben. Doch jede Variable zeigt auf ein anderes Objekt, nämlich "Alex", "Valnar", und "Grandy".
Wir haben zu Beginn ja schon festgestellt, dass jeder Baum anders aussieht. Mancher mit vielen Blättern, andere mit wenigen. Mancher mit einem dicken Stamm, andere mit dünnen. Auf diese Eigenschaften verweisen die Instanzvariablen. Jeder Baum hat eine Variable @stamm. Aber bei einem Baum verweist sie auf den Durchmesser 1 Meter, bei einem anderen Baum auf den Durchmesser 0.5 Meter. Genauso können Autos verschiedene Farben, Kennziffern etc. haben.
Instanzvariablen verweisen also auf Objekte, die in jeder Instanz unterschiedlich sind.
Klassenvariablen
Erinnern wir uns an die erste Regel: "Alles in Ruby ist ein Objekt". Demnach sind also auch Klassen, Methoden etc. Objekte? Korrekt! Und sie verhalten sich wie auch alle anderen Objekte. So ist eine Klasse ebenfalls nur eine Instanz, die aus einer anderen Klasse namens Class abgeleitet ist. Und auch sie besteht aus verschiedenen anderen Objekten. Unser Held alex besaß Instanzvariablen, die nur er, nicht aber seine Klasse besaß. Da die Klasse aber auch ein Objekt ist, muss sie doch auch Instanzvariablen besitzen können, die nur sie, und keine andere Klasse hat. Und das geht auch. Diese Art von Variablen nennt sich Klassenvariable. Sie ist im Prinzip eine Instanzvariable der Klasse.
Eine Klassenvariable beginnt mit den Zeichen @@
Klassenvariablen müssen, genau wie lokale Variablen, initialisiert werden. Sie sind also nicht von Anfang an nil. Da eine Klassenvariable an ihre Klasse gebunden ist, muss sie auch dort initialisiert werden.
class Held @@heldenzahl = 0 end
Wir schreiben also eine Klassenvariable nicht innerhalb einer Methodendefinition, sondern innerhalb der Klassendefinition.
Da Klassenvariablen an eine Klasse gebunden sind, und Klassen nie gelöscht werden, sind sie also genau wie globale Variablen "unsterblich", sie werden also nie gelöscht. Was unterscheidet sie dann von globalen Variablen? Ihr Zugriff! Auf eine Klassenvariable darf man nur innerhalb der Klassendefinition, einer Klassenmethode oder von einer Instanz der Klasse aus zugreifen.
class Held @@heldenzahl = 0 end print @@heldenzahl #=>uninitialized class Variable
Dieser Code führt also zu einer Fehlermeldung, da man versucht, von außerhalb der Klasse Held auf eine Klassenvariable von Held zuzugreifen.
Innerhalb der Methoden der Klasse darf man hingegen auf die Klassenvariable zugreifen.
class Held @@heldenzahl = 0 def initialize @@heldenzahl += 1 print("Es gibt bereits ", @@heldenzahl, " Helden auf der Welt!") end end alex = Held.new #=>Es gibt bereits 1 Helden auf der Welt valnar = Held.new #=>Es gibt bereits 2 Helden auf der Welt grandy = Held.new #=>Es gibt bereits 3 Helden auf der Welt
In diesem Script zum Beispiel wird immer dann, wenn eine neue Instanz von Held erzeugt wird, die Klassenvariable @@heldenzahl um 1 erhöht. Wenn man drei Helden erzeugt, beträgt die Variable also 3. Jeder dieser Helden kann auf die Klassenvariable zugreifen. Im Gegensatz zu den Instanzvariablen verweist aber die Klassenvariable jeder Instanz auf das gleiche Objekt.
class Auto @@raeder = 4 def raederanzahl print("Das Auto hat ", @@raeder, " Räder.") end end opel = Auto.new porsche = Auto.new mercedes = Auto.new opel.raederanzahl #=>Das Auto hat 4 Räder porsche.raederanzahl #=>Das Auto hat 4 Räder mercedes.raederanzahl #=>Das Auto hat 4 Räder
Dieses Beispiel soll noch einmal verdeutlichen, dass alle Instanzen von der gleichen Klasse auch die gleichen Klassenvariablen teilen.
Konstanten
Der letzte Variablentyp bezeichnet die Konstanten. Dies sind Variablen, die, genau wie globale Variablen, nie gelöscht werden und auf die man von überall aus zugreifen kann.
Jede Variable, die mit einem Grossbuchstaben beginnt, ist eine Konstante.
Im Gegensatz zu globalen Variablen können Konstanten in Module und Klassen gepackt werden. So lassen sich Konstanten strukturieren. Um auf eine Konstante zuzugreifen, die in ein Modul oder in eine Klasse eingelagert ist, schreibt man Klassen_oder_Modulname::Konstantenname
class Held HEROISCHE_TATEN = ["Drachen jagen", "Prinzessinen befreien", "Welt retten", "Dämonen töten"] def was_macht_ein_held? print(HEROISCHE_TATEN) end end print(Held::HEROISCHE_TATEN)
Wie man an diesem Beispiel sieht, müssen Konstanten einfach nur innerhalb einer Klassen- oder Moduldefinition deklariert werden. Üblicherweise schreibt man Konstanten komplett in Großbuchstaben, aber das ist nur eine Frage des schönen Programmierstils. Man sieht in diesem Beispiel auch, dass man innerhalb einer Klasse die dort enthaltenen Konstanten direkt aufrufen kann, während man von außerhalb der Klasse immer Klassenname::Konstantenname schreiben muss.
Konstanten sind eigentlich, wie der Name schon sagt, konstante, also unveränderliche Werte. Einmal festgelegt sollte eine Konstante nicht mehr geändert werden. In Ruby ist es möglich Konstanten zu ändern. Aber man sollte von dieser Möglichkeit keinen Gebrauch machen. Konstanten werden genutzt um bestimmte Werte, die immer wieder im Code gebraucht werden und die während des Ablaufes des Programms nie geändert werden müssen, in eine Variable festzuhalten. Das typischste Beispiel für eine Konstante wäre die mathematische Konstante PI.
Konstanten lassen sich aber auch als Steuerelemente nutzen:
class Held ROMANTISCH = 1 FLUECHTIG = 2 ZAERTLICH = 3 KUSS = " küsst die Prinzessin " def initialize(name) @name = name end def kuesse_prinzessin(kussform) case kussform when ROMANTISCH then print(@name, KUSS, "bei Kerzenschein.") when FLUECHTIG then print(@name, KUSS, "schnell noch vor dem Zähneputzen.") when ZAERTLICH then print(@name, KUSS, "voller Leidenschaft.") end end end alex = Held.new("Alex") alex.kuesse_prinzessin(Held::ROMANTISCH) #=>Alex küsst die Prinzessin bei Kerzenschein
Natürlich hätte man auch schreiben können alex.kuesse_prinzessin(1). Aber oft genug wurde gepredigt, dass der Code möglichst übersichtlich und selbsterklärend sein soll. Wenn man Wochen später sich diesen Code noch einmal anguckt, weiß man dann, was mit der Zahl 1 gemeint ist? Die Konstante ROMANTISCH hingegen erklärt sogleich, was ihr Inhalt für eine Wirkung hat.
Das Input-Modul des Makers arbeitet ebenfalls mit solchen Steuerkonstanten. Dieses Modul kann überprüfen, welche Taste soeben gedrückt wurde. Dies geschieht beispielsweise über die Methode press?. Als Parameter übergibt man dieser Methode, welche Taste abgefragt werden soll. Will man also wissen, ob der Spieler gerade die Shift-Taste gedrückt hat, so schreibt man:
Input.press?(Input::SHIFT)
Was verbirgt sich hinter dieser Konstante Shift? Probieren wir es einfach aus:
print(Input::SHIFT) #=>21
Man hätte also nur 21 statt Input::SHIFT als Parameter angeben müssen. Das Ergebnis wäre das Gleiche geblieben. Doch lässt es sich doch wesentlich einfacher merken, dass SHIFT für die Shift-Taste steht, als das hierfür die Nummer 21 gebraucht wird.
Den wichtigsten Verwendungszweck von Konstanten haben wir aber bisher noch ausgelassen. Die erste OOP-Regel besagt: Alles in Ruby ist ein Objekt. Also sind auch Klassen und Module Objekte. Wir haben außerdem gesagt, dass jedes Objekt eine Variable braucht, um vor dem hungrigen GC (Garbage Collector) verschont zu werden. Wir haben außerdem gesagt, dass Klassennamen und Modulnamen mit Großbuchstaben anfangen müssen. Zählt man diese Fakten zusammen, kommt man zum sicheren Schluss: Module und Klassen sind Konstanten!
Tatsächlich verhalten sie sich auch wie Konstanten. Module und Klassen können nie gelöscht werden oder ihre Zuweisung verlieren. Sie sind also unsterblich. Gleichsam kann man von jedem Ort aus auf Klassen und Module zugreifen. Und man kann Klassen und Module ineinander verpacken.
class Literatur def initialize print("Ich kaufe mir ein Buch!") end class Dramatik module Theater def self.besuchen print("Ich besuche eine Theatervorführung!") end end def initialize print("Ich lese Hamlet") end end class Lyrik def initialize print("Ich mag lieber Gedichte!") end end end buch = Literatur.new #=> Ich kaufe mir ein Buch! drama = Literatur::Dramatik.new #=> Ich lese Hamlet gedicht = Literatur::Lyrik.new #=> Ich mag lieber Gedichte! theaterstück = Literatur::Dramatik::Theater.besuchen #=> Ich besuche eine Theatervorführung!
Dieses Beispiel mag etwas verwirrend sein, es zeigt aber wunderbar wie man Konstanten ineinander verpacken kann. Übrigens ist es nicht ratsam, Klassen ineinander zu verpacken. Das ist kein guter Stil. Bei Modulen ist das aber gang und gäbe. So gibt es im Maker das Modul RPG. Es enthält selbst viele andere Klassen und Module, welche für grundlegende Funktionen und das Abspeichern von Daten des Makers verantwortlich sind.
Zusammenfassung
Hier noch einmal eine kurze Zusammenfassung zum Thema Variablen.
| Variablentyp | Syntax-Beispiele | Lebensdauer | Zugriffsrecht |
|---|---|---|---|
| lokale Variablen | held, _name | nur während eines Methoden- oder Blockablaufs | nur innerhalb der Methode bzw. des Klassen-, oder Iteratorblocks |
| globale Variablen | $game_variables, $firlefanz | während des gesamten Programmablaufs | Keine Beschränkung |
| Instanzvariablen | @fenster, @_heldenname | Abhängig von der Instanz | nur innerhalb der Instanz |
| Klassenvariablen | @@anzahl, @@default_name | während des gesamten Programmablaufs | nur innerhalb der Klasse, den Subklassen und ihren Instanzen |
| Konstanten | PI, HALBWERTSZEIT | während des gesamten Programmablaufs | außerhalb der Klasse muss der Klassenname und :: davorgeschrieben werden |
Desweiteren noch ein Beispiel, in dem alle Variablentypen vorkommen:
class Held #Klassenvariablen müssen in der Klassendefinition deklariert werden @@heldenanzahl = 0 #Klassenvariable #Konstanten müssen in der Klassedefinition deklariert werden MAGIER = 1 KRIEGER = 2 HEILER = 3 def initialize(name, klasse) #name ist wie alle Parameter eine lokale Variable @@heldenanazhl += 1 @name = name #@name ist eine Instanzvariable #Instanzvariablen werden gewöhnlich in der initialize Methode deklariert. print("Der ", @@heldenanzahl, "te Held erblickt das Licht der Welt!") case klasse #innerhalb der Klasse reicht es, den Konstantennamen anzugeben when MAGIER then print("Er beherrscht mächtige Magie!") when KRIEGER then print("Sein Schwert ist schärfer als sein Verstand!") when HEILER then print("Fürsorge ist heldenhafter als Mut und Tapferkeit") end end end #Außerhalb der jeweiligen Klasse muss vor dem Konstantennamen der Klassenname stehen grandy = Held.new("Alex", Held::KRIEGER) #=> Der 1te Held erblickt das Licht der Welt! #=> Sein Schwert ist schärfer als sein Verstand! libra = Held.new("Libra", Held::MAGIER) #=> Der 2te Held erblickt das Licht der Welt! #=> Er beherrscht mächtige Magie! dankwart = Held.new("Dankwart", Held::HEILER) #=> Der 3te Held erblickt das Licht der Welt! #=> Fürsorge ist heldenhafter als Mut und Tapferkeit
Spezielle Variable self
In Ruby gibt es einige besondere Variablen, die wir zum größten Teil schon einmal in einem anderen Zusammenhang besprochen haben. Diese speziellen Variablen sind true, welche auf eine Instanz der Klasse TrueClass verweist, false, eine Instanz von FalseClass, sowie nil, die auf eine Instanz von NilClass zeigt.
Eine Variable, die wir bisher noch nicht besprochen haben, ist die Variable self. Sie nimmt im Scripteditor eine gesonderte Färbung an, was bereits darauf hinweist, dass dies keine normale Variable sein kann. self verweist immer auf das Objekt, welches die momentane Methode ausführt.
class Held def wer_bin_ich? print(self) end end alex = Held.new alex.wer_bin_ich? #=> <Held:...>
In diesem Beispiel wird die Methode wer_bin_ich? ausgeführt. self verweist also auf das Objekt, welches die Methode ausführt. Also auf alex. Als Ausgabe kommt bei mir <Held:0x153e500>. Bei euch wird sicherlich eine andere Zahl erscheinen, aber das ist unwichtig. Die Zahlen im Zusammenhang mit Held weisen daraufhin, dass es sich um eine Instanz der Klasse Held handelt.
class Held print(self) #=> Held end
In diesem Beispiel befindet sich die Variable self nicht mehr innerhalb einer Methode, sondern innerhalb des Klassenrumpfes. Sie wird nun nicht mehr von einer Instanz von Held, sondern von der Klasse Held selber ausgeführt.
Wofür braucht man die Methode self? Da wäre einmal das Problem der Mehrdeutigkeit bei lokalen Variablen und Methoden.
def test "Dies ist ein Test" end test = 5 print(test) #=> 5
In diesem Beispiel gibt es eine lokale Variable namens test, sowie eine Methode, die ebenfalls test heißt. Woher soll der Maker wissen, ob man mit print(test) nun die Methode oder die Variable meint. In solchen Fällen gibt Ruby der Variable immer den Vorzug. Es gibt aber zwei Möglichkeiten, dies zu vermeiden.
print test()
Wir haben früher schon einmal gesagt, man solle bei Methoden möglichst Klammern benutzen, selbst dann, wenn die Methode keine Parameter verlangt. An den Klammern erkennt Ruby, dass es sich um eine Methode und keine Variable handelt.
Man kann aber auch die Variable self benutzen:
print(self.test)
Auch dadurch weiß Ruby, dass das Objekt angesprochen wird, nicht die Variable.
Eine andere und wesentlich bedeutungsvollere Anwendungsmethode wird im folgenden Kapitel verraten.
Mehr zu Methoden
Über Methoden gibt es immer wieder neues zu lernen. Auch in den späteren Kursteilen wird sich dies nicht ändern. Bisher haben wir nur mit Methoden gearbeitet, die zu Instanzen gehören, bzw. von diesen ausgeführt werden können. Aber genauso wie es auch Klassenvariablen gibt, die von der Klasse benutzt werden können, gibt es auch Klassenmethoden, die der Klasse zugänglich sind. Damit Ruby sie von normalen Instanzmethoden unterscheiden kann, beginnen Klassenmethoden immer mit dem Namen der Klasse.
class Held def Held.definition print("Ein Held ist Hauptfigur von Rollenspielen, Geschichten, Legenden und Sagen.") end end Held.definition #=> Ein Held ist Hauptfigur von Rollenspielen, Geschichten, Legenden und Sagen.
Klassenmethoden können von Klassen genutzt werden, nicht aber von ihren Instanzen. Folgender Code würde also zu einer Fehlermeldung führen:
alex = Held.new alex.definition #=>No Method Error
Klassenmethoden können für all jene Dinge genutzt werden, die zum Themenbereich der Klasse passen, welche auch außerhalb der Klasse und Instanzen genutzt werden sollen, allerdings keine Instanz und deren Instanzvariablen brauchen.
Wenn also irgendwo im Spiel jemand die Definition für Held wissen will, muss man doch nicht extra einen neuen Helden erzeugen. Stattdessen nutzt man Klassenmethoden.
Bisher haben wir Code nur außerhalb von Klassen, oder innerhalb von Methodendefinitionen geschrieben. Man kann Code allerdings auch innerhalb von Klassendefinitionen schreiben.
class Test print("Hallo!") end
Der Code wird ebenfalls zum Beginn des Projektes ausgeführt. Worin liegt also der Unterschied, ob man ihn außerhalb der Klasse, oder innerhalb der Klasse schreibt? Das folgende Experiment soll diese Frage klären:
class Test #innerahalb der Klasse print(self) #=> Test end #außerhalb der Klasse print(self) #=> nil
Der Unterschied besteht lediglich darin, wer den Code ausführt. In objektorientierten Sprachen muss jede Methode von einem Objekt ausgeführt werden. Innerhalb der Klasse wird die Methode print von der Klasse selbst ausgeführt. Außerhalb wird sie von einem unsichtbaren Hauptprogrammm-Objekt ausgeführt, welches sich hinter dem Wort nil verbirgt. Man "könnte" also auch schreiben:
Test.print(self) nil.print(self)
(Dieses Codebeispiel ergibt allerdings eine Fehlermeldung, wegen einer Sache, die mit diesem kapitel nichts zu tun hat und die auch nicht wichtig für unseren Kurs ist.) Was so ziemlich auf das gleiche hinausgelaufen wäre. Dieses Wissen jedoch kann ziemlich nützlich sein. Wenn innerhalb der Klasse alle Methoden von der Klasse selbst ausgeführt werden, braucht man innerhalb der Methode zum Ausführen von Klassenmethoden nicht den Klassennamen vor die Methode zu schreiben:
class Test def Test.beispiel print("Dies ist ein Beispiel!") end beispiel #=> Dies ist ein Beispiel end Test.beispiel #=> Dies ist ein Beispiel
In den späteren Kapiteln werden wir einige wichtige Klassenmethoden kennenlernen. Beispielsweise attr_accessor, private, module_function usw. Man sollte sich immer vergegenwärtigen, dass diese Methoden der Klasse gehören und auch nur von ihr ausgeführt werden können. Man muss allein deshalb nicht Klasse.attr_accessor schreiben, weil innerhalb der Klassendefinition ohnehin automatisch jede Methode von der Klasse ausgeführt wird.
Das Wissen lässt sich auch für eine weitere Anwendung nutzen. Die Variable self verweist immer auf das Objekt, welches die Methode ausführt. Innerhalb einer Klassendefinition wird jede Methode von der Klasse ausgeführt. self verweist also immer auf den Klassennamen. Aus diesem Grund kann man schreiben:
class Supertolle_Testklasse def self.beispiel #ist das gleiche wie Supertolle_Testklasse.beispiel print("Dies ist ein Beispiel") end end Supertolle_Testklasse.beispiel
Bei längeren Klassennamen ist es schon eine Erleichterung, einfach nur self hinzuschreiben (außerhalb der Klassendefinition lässt sich das allerdings nicht vermeiden).
Die nächste Neuerung bezüglich der Methoden betreffen die Parameter. In Ruby ist es möglich, optionale Parameter mit Standardwerten zu verwenden.
def vorstellen(name=false) if name then print("Ich heiße ", name) else print("Ich habe noch keinen Namen") end end vorstellen() #=>Ich habe noch keinen Namen vorstellen("Alex") #=>Ich heiße Alex
In diesem Fall ist der Parameter name optional. Das heißt, man muss ihn nicht angeben, um die Methode aufzurufen. Wird kein Parameter übergeben, so wird stattdessen der Standardwert (in diesem Beispiel false) verwendet. Wird ein Parameter angegeben, so wird dieser statt dem Standardwert ganz normal als lokale Variable verwendet.
Die Syntax für optionale Parameter lautet:
def methodenname(parameter1, parameter2, optionaler_parameter1 = standardwert1, optionaler_parameter2 = standardwert2)
Eine Methode kann beliebig viele optionale Parameter haben. Wichtig ist nur, dass die optionalen Parameter hinter die normalen, notwendigen Parameter geschrieben werden!
Desweiteren wollen wir noch zwei spezielle Methoden kennenlernen: Object#to_s() und Object#inspect(). Beide Methoden wandeln ein beliebiges Objekt in einen String um und werden von den Methoden print() und p() benutzt um Objekte als Textnachricht darzustellen. print() benutzt dabei die Methode to_s(), während p() die Methode inspect() verwendet. Darum werden manche Objekte wie Arrays oder Strings bei der Methode p() anders dargestellt als bei der Methode print().
Wir wollen uns nur mit to_s() beschäftigen, da dies die wichtigere Methode ist. Da to_s() in der Klasse Object definiert ist, beherrscht jedes Objekt diese Methode. Allerdings ist die Ausgabe bei den meisten Objekten eher hässlich. Darum ist es lohnenswert die Methode to_s() in einer eigenen Klasse zu überschreiben.
class Held def initialize(name, klasse, level) @name = name @klasse = klasse @level = level end end alex = Held.new("Alex", "Krieger", 25) print alex # => #<Held:xxx @klasse="Krieger", @name="Alex" @level=25> # Das sieht hässlich aus. Daher schreiben wir unsere eigene to_s Methode: class Held def to_s() "#{@name} ist ein #{@klasse} Stufe #{@level}" end end print alex # => Alex ist ein Krieger Stufe 25 # Na das sieht doch schon deutlich schöner aus
Am Ende dieses Kapitels wollen wir uns noch kurz mit Sonderzeichen innerhalb von Methodennamen beschäftigen. Nicht jedes Sonderzeichen ist für Methodennamen erlaubt. Alle Sonderzeichen jedoch, die von Standardmethoden (+, -, ==, =, << usw.) genutzt werden, können auch für eigene Methodennamen genommen werden.
class Held def +(anderer_held) print(self, " und ", anderer_held, " arbeiten jetzt zusammen!") end end alex = Held.new frieda = Held.new alex + frieda #=> Held:... und Held:... arbeiten jetzt zusammen!
Man kann Methoden wie +, -, <<, == usw. also selbst überschreiben. Die Methode = übrigens lässt sich ebenfalls verwenden, allerdings nur, wenn sie nach einem standardmäßigen Methodennamen kommt.
class Held def lieblingsessen=(futter) print(self, "'s Lieblingsspeise ist ", futter) end end alex = Held.new alex.lieblingsessen = "Spinat" #=> Held:...'s Lieblingsspeise ist Spinat
Bei solchen Methode kann aber schnell etwas schiefgehen:
class Held def lieblingsessen=(futter) print(self, "'s Lieblingsspeise ist ", futter) end def verwandlung_zum_werwolf #Nun isst der Held lieber rohes Fleisch! #Schreiben wir das so? lieblingsessen = "Rohes Fleisch" #oder lieber doch so? self.lieblingsessen = "Rohes Fleisch" end end
Tja, was ist richtig? Reicht es aus zu schreiben...?
lieblingsessen = "Rohes Fleisch"Nein, denn woher weiß Ruby ob lieblingsessen = eine lokale Variable oder eine Methode ist. Wir haben schon im Kapitel self über solche Verwechslungen gesprochen. In diesem Fall muss zwingend ein self vor den Methodennamen geschrieben werden. self.lieblingsessen = "Rohes Fleisch"
Symbole
Wir haben im ersten Kurs ja bereits die wichtigsten Rubyklassen gelernt. Die Klasse String beschreibt Zeichenketten (Wörter, Sätze, Buchstaben etc.), die Klasse Fixnum beschreibt kleine Zahlen, Float beschreibt Kommazahlen, TrueClass, FalseClass und NilClass sind die Klassen unserer Variablen true, false und nil, Array beschreibt Reihen/Listen von Objekten und letztlich gab es noch Hashs, die zwei Objekte miteinander verknüpfen können.
Allerdings gibt es noch weitere Standardklassen in Ruby, die wir nach und nach im Kurs durchsprechen werden. Eine dieser weiteren Klassen ist Symbol.
Ein Symbol repräsentiert den Namen einer Methode oder einer Variable. Z.B. hat die Variable @name das Symbol :@name. Die Methode lieblingsessen= hat das Symbol :lieblingsessen=. Man kennzeichnet Symbole mit einem Doppelpunkt vor dem Symbol, statt wie bei Strings mit Anführungszeichen.
Tja, das war eigentlich auch schon alles, was es dazu zu sagen gibt. Symbole sind für dynamische Methodenerstellung, Variablenbelegung etc. sehr praktisch. Aber mit sowas wollen wir uns im Kurs gar nicht rumschlagen. Merkt euch einfach: Wenn im Scripteditor etwas mit einem Doppelpunkt auftaucht, dann handelt es sich dabei um ein Symbol.
Symbole sind normale Objekte die man in Variablen speichern kann etc.
mein_symbol = :irgendein_symbol viel_symbole = [:a, :b, :c]
Im nächsten Kapitel werdet ihr ein Anwendungsgebiet für Symbole kennenlernen.
Attribute
Nun fragt sich der eine oder andere vielleicht, was man mit Symbolen, also mit den Namen von Variablen und Methoden anfangen soll. Dieses Kapitel liefert die Antwort. Zum Thema Variablen haben wir bereits gesagt, dass Klassenvariablen auf ihre Klasse und deren Instanzen beschränkt sind. Instanzvariablen sind sogar nur auf die Instanzen beschränkt.
class Held @@heldenanzahl = 0 def initialize(name) @@heldenanzahl += 1 @name = name end end held = Held.new("Alex") heldin = Held.new("Frieda")
In diesem Beispiel erzeugen wir zwei Helden: Alex und Frieda. Diese beiden Helden treffen sich zu einem Kaffeekränzchen und wollen den Namen des jeweils anderen wissen. Nun haben wir aber gesagt, dass eine Instanzvariable nur innerhalb einer Instanz verwendet werden darf. Dieser Code wäre also nicht möglich:
print(@name) #=>nil print(held.name) #=>Error: undefined method
Wie also soll Frieda ihren Namen verraten? Nun, dies lässt sich einfach mit einer Methode regeln, welche den Heldennamen als Ausgabewert hat. Wir fügen dem obigen Code also hinzu:
class Held def name @name end def heldenanzahl @@heldenanzahl end end heldin = Held.new("Frieda") print(heldin.name) #=> Frieda print(heldin.heldenanzahl) #=> 1
Nun wird die Methode name aufgerufen, welche die Instanzvariable @name als Rückgabewert hat.
Methoden die Zugriff auf eine Instanzvariable ermöglichen, bezeichnet man als Getter-Methoden!
Das gleiche funktioniert auch bei Klassenvariablen.
Nachdem Frieda Ärger mit der lokalen Verbecherorganisation bekommen hat, will sie sich in Heidi umbenennen lassen. Instanzvariablen dürfen nur innerhalb einer Klasse geändert werden. Wieder müssten wir eine Methode schreiben:
class Held def aendere_name(neuer_name) @name = neuer_name end end heldin = Held.new("Frieda") print(heldin.aendere_name("Heidi")) #=> Heidi
Wir können uns allerdings auch die Sonderzeichenregel des Makers zu Nutze machen.
class Held def name=(neuer_name) @name = neuer_name end end heldin = Held.new("Frieda") heldin.name = "Heidi"
Wir erinnern uns: Trägt eine Methode am Ende ihres Namens ein =, so kann dieses getrennt vom Methodenname aufgeschrieben werden. Statt heldin.name=("Heidi") darf man also auch heldin.name = "Heidi" schreiben.
Methoden, die einer Instanzvariable einen neuen, per Parameter mitgelieferten, Wert zuweisen, bezeichnet man als Setter-Methoden
Auf diese Weise werden aus zuvor privaten, den Instanzen vorbehaltenen Variablen, öffentliche, auch anderen Objekten zugängliche Variablen. Man wird in Ruby sehr oft solche Getter- und Setter-Methoden brauchen, denn häufig müssen Objekte untereinander ihre Instanzvariablen austauschen. Aus diesem Grund gibt es Methoden, welche diese ganze Prozedur vereinfacht. Sie heißen attr_accessor, attr_reader und attr_writer.
Was machen diese Methoden? Nun, eigentlich nichts anderes als was wir eben gemacht haben.
class Held attr_reader(:name) attr_accessor(:waffe) attr_writer(:quest) def initialize(name, waffe) @name = name @waffe = waffe @quest = "unbeschäftigt" end def beschaeftigung print(@name, " ist gerade ", @quest) end end heldin = Held.new("Frieda", "Schwert") print(heldin.name) #=> Frieda print(heldin.waffe) #=> Schwert heldin.waffe = "Morgenstern" print(heldin.waffe) #=> Morgenstern heldin.quest = "Drachen jagen" print(heldin.beschaeftigung) #=> Frieda ist gerade Drachen jagen
Die attr_writer (Attributschreiber) Methode erzeugt in seiner Klasse eine quest= Methode, mit der man die Instanzvariable @quest umschreiben kann (also eine Setter-Methode). attr_reader erzeugt eine name Methode, mit der man die Instanzvariable @name ausgeben kann (also eine Getter-Methode). attr_accessor erzeugt beide Methoden zum Ausgeben und Schreiben gleichzeitig, also eine waffe= und eine waffe Methode.
Als Parameter übergibt man der Methode einfach nur die Symbole der jeweiligen Instanzvariablen - allerdings ohne das @ am Anfang. Man kann ihr beliebig viele Parameter übergeben. Die Methode sucht sich die dazugehörigen Instanzvariablen heraus und erzeugt Zugriffsmethoden für diese.
class Beispiel attr_accessor(:a, :b, :c) end mein_beispiel = Beispiel.new mein_beispiel.a = 10 mein_beispiel.b = "Hallo" mein_beispiel.c = 5.23 print(mein_beispiel.a, mein_beispiel.b, mein_beispiel.c) #=> 10Hallo5.23
Wichtig ist, dass diese Methoden in den Klassenrumpf geschrieben werden. Denn diese Methoden gehören zur Klasse, nicht zur Instanz. Schließlich fügen sie der Klasse neue Methoden hinzu, welche von jeder Instanz der Klasse genutzt werden können. Die attr-Methoden funktionieren übrigens nur für Instanzvariablen. Bei Klassenvariablen muss man eigene Methoden schreiben (oder man schreibt eine neue attr-Methode, die auch für Klassenvariablen gilt. Aber derart komplizierte Themen sollen vorerst nicht besprochen werden).
Wer den Scripteditor des Makers schon einmal beäugt hat, wird dort massenweise dieser attr-Methoden finden. Sie demonstrieren, wie dynamisch Ruby arbeitet. Jedes Objekt in Ruby, einschließlich die Klassen, kann jederzeit verändert werden. Man kann Ruby also, während es ausgeführt wird, umprogrammieren.
Wir wollen nun ein Handelssystem programmieren. Dafür verwenden wir folgende Klassen mit den jeweiligen Attributen und Methoden (von initialize abgesehen):
- Item, Attribute: name, preis Methoden: to_s
- Waffe < Item, Attribute: schaden
- Trank < Item, Attribute: hp_heilung, mp_heilung
- Shop, Attribute: geld, items Methoden: hat_item?(), kaufe(), verkaufe()
- Ein Händler darf ein Item nur durch den Kauf von einem anderen Händler erwerben
- Ein Händler darf nur dann ein Item kaufen, wenn der Verkäufer dieses Item besitzt und der Händler genug Geld hat, um es zu bezahlen
# Waffen schwert = Waffe.new("Schwert", 100, 10) lanze = Waffe.new("Lanze", 150, 15) hellebarde = Waffe.new("Hellebarde", 250, 20) # Tränke heiltrank = Trank.new("Heiltrank", 25, 100, 0) manatrank = Trank.new("Manatrank", 50, 0, 50) alraunsirup = Trank.new("Alraunsirup", 250, 100, 100) # Händler alchemist = Shop.new([heiltrank, heiltrank, manatrank, manatrank, alraunsirup], 500) schmied = Shop.new([schwert, schwert, schwert, lanze, hellebarde], 200) # Schmied kauft sich zwei Heiltränke schmied.kaufe(heiltrank, alchemist) schmied.kaufe(heiltrank, alchemist) # Alchemist kauft ein Schwert vom Schmied alchemist.kaufe(schwert, schmied) # Wie viel Geld haben beide Händler noch? print "Schmied: #{schmied.geld}€" # => Schmied: 250€ print "Alchemist: #{alchemist.geld}€" # => Alchemist: 450€ # was für Items haben beide Händler? print schmied.items.join(",") # => Schwert, Schwert, Lanze, Hellebarde, Heiltrank print alchemist.items.join(",") # => Manatrank, Manatrank, Alraunsirup, Schwert
Tipps:
- welche Position ein Element in einem Array hat, kann man mit Array#index() abfragen
- ein Element löscht man aus einem Array mit Array#delete_at()
- nicht für jedes Attribut muss man attr_accessor() benutzen. Oft reicht auch ein einfaches attr_reader()
- wenn ein Händler A von einem Händler B ein Item kauft, ist das dasselbe als ob Händler B dem Händler A ein Item verkauft. Berücksichtigt das bei der Definition eurer Methoden.
- Item#to_s() sollte den Namen des Items ausgeben
- Array#join(trenner) wandelt einen Array in einen String um, wobei das trenner-Zeichen die einzelnen Elemente voneinander abgrenzt.
Erweitert die Waffen-, und Trank-Klassen um eine Methode leistung(), welche das Preis-Leistungsverhältnis des jeweiligen Items berechnet. Gebt jedem Händler eine Methode empfehlung(), welche das günstigste Item (also das mit dem höchsten Preisleistungsverhältnis) zurückgibt.
Schreibt eine Schmied < Shop und Alchemist < Shop Klasse, welche nur Waffen bzw. Tränke kaufen und verkaufen. Versucht mit super() zu arbeiten um möglichst wenig Code neu schreiben zu müssen.


