09. Klassen

Bisher haben wir viele verschiedene Arten von Objekten oder Klassen gesehen: Strings, Integers, Floats, Arrays und einige spezielle Objekte (true, false und nil), auf die wir später zurückkommen werden. In Ruby beginnen diese Klassen immer mit einem Großbuchstaben: String, Integer (Ganzzahlen), Float (Gleitkommazahlen), Array (Felder) usw. Wenn wir ein neues Objekt einer bestimmten Klasse erstellen wollen, verwenden wir im Allgemeinen new:

a = Array.new  + [12345]  # Addition von Arrays.
b = String.new + 'hallo'  # Addition mit Strings.
c = Time.new

puts 'a = '+a.to_s
puts 'b = '+b.to_s
puts 'c = '+c.to_s

Da wir Arrays und Strings mit [...] bzw. '...' erstellen können, verwenden wir dafür selten new (Auf jeden Fall ist im vorherigen Beispiel nicht ganz klar, dass String.new einen leeren String und Array.new ein leeres Array erstellt). Zahlen sind jedoch eine Ausnahme: Sie können einen Integer nicht mit Integer.new erstellen. Sie müssen einfach die Zahl eingeben.

Die Klasse Time

Okay, und die Klasse Time? Time-Objekte repräsentieren Zeitpunkte. Sie können Zahlen zu Zeiten addieren (oder davon subtrahieren), um neue Zeitpunkte zu erhalten: Das Addieren von 1.5 zu einer Zeit gibt einen neuen Zeitpunkt eineinhalb Sekunden später zurück:

zeit  = Time.new    # Der Zeitpunkt, an dem Sie diese Seite laden.
zeit2 = zeit + 60   # Eine Minute später.

puts zeit
puts zeit2

Sie können auch eine Zeit für einen bestimmten Moment mit Time.mktime erstellen:

puts Time.mktime(2000, 1, 1)          # Jahr 2000.
puts Time.mktime(1976, 8, 3, 10, 11)  # Jahr, in dem ich geboren wurde.

Hinweis: Als ich geboren wurde, galt die Pacific Daylight Time (PDT). Als das Jahr 2000 kam, galt jedoch die Pacific Standard Time (PST), zumindest für uns an der Westküste. Die Klammern dienen dazu, die Parameter für mktime zu gruppieren. Je mehr Parameter Sie hinzufügen, desto genauer wird Ihr Zeitpunkt.

Sie können zwei Zeiten mit den Vergleichsmethoden vergleichen (eine frühere Zeit ist kleiner als eine spätere Zeit).

Ein paar Dinge zum Ausprobieren

  • Eine Milliarde Sekunden... Finden Sie die genaue Sekunde Ihrer Geburt heraus (wenn Sie können). Finden Sie heraus, wann Sie eine Milliarde Sekunden alt werden (oder vielleicht wann Sie es wurden?). Markieren Sie es dann in Ihrem Kalender.
  • Happy Birthday! Fragen Sie, in welchem Jahr eine Person geboren wurde. Fragen Sie dann nach dem Monat und schließlich nach dem Tag. Finden Sie dann heraus, wie alt diese Person ist, und geben Sie ihr einen KLAPS! für jeden Geburtstag, den sie hatte.

Die Klasse Hash

Eine weitere sehr nützliche Klasse ist die Klasse Hash. Hashes sind Arrays sehr ähnlich: Sie haben eine Menge Plätze, die auf verschiedene Objekte zeigen können. In einem Array sind die Plätze jedoch in einer Reihe angeordnet, und jeder ist nummeriert (beginnend bei Null). In einem Hash sind die Plätze jedoch nicht in einer Reihe angeordnet (sie sind einfach irgendwie alle zusammen), und Sie können jedes Objekt verwenden, um auf einen Platz zu verweisen, nicht nur eine Zahl. Es ist gut, Hashes zu verwenden, wenn Sie eine Menge Dinge haben, die Sie im Auge behalten wollen, die aber nicht wirklich in eine geordnete Liste passen. Zum Beispiel die Farben, die ich in verschiedenen Teilen dieses Tutorials verwende:

farbArray = []  # das gleiche wie Array.new
farbHash  = {}  # das gleiche wie Hash.new

farbArray[0]         = 'rot'
farbArray[1]         = 'grün'
farbArray[2]         = 'blau'
farbHash['strings']  = 'rot'
farbHash['zahlen']   = 'grün'
farbHash['schluesselwoerter'] = 'blau'

farbArray.each do |farbe|
  puts farbe
end
farbHash.each do |codeTyp, farbe|
  puts codeTyp + ':  ' + farbe
end

Wenn ich ein Array verwende, muss ich mich daran erinnern, dass Platz 0 für Strings ist, Platz 1 für Zahlen usw. Aber wenn ich einen Hash verwende, ist es einfach! Der Platz 'strings' speichert natürlich die Farbe von Strings. Nichts zu merken. Sie haben vielleicht bemerkt, dass die Objekte im Hash, als ich each verwendete, nicht in derselben Reihenfolge herauskamen, in der ich sie eingegeben habe (Zumindest nicht, als ich das geschrieben habe. Vielleicht tun sie es jetzt... man weiß nie bei Hashes). Arrays sind dazu da, Dinge in Ordnung zu halten; Hashes nicht.

Obwohl die Leute normalerweise Strings verwenden, um die Plätze in einem Hash zu benennen, können Sie jede Art von Objekt verwenden, sogar Arrays und andere Hashes (obwohl mir kein Grund einfällt, warum Sie das tun wollen würden...):

seltsamerHash = Hash.new

seltsamerHash[12] = 'affen'
seltsamerHash[[]] = 'leere'
seltsamerHash[Time.new] = 'nichts geht über die Gegenwart'

Hashes und Arrays sind gut für verschiedene Dinge: Es liegt an Ihnen zu entscheiden, welches für Ihr spezielles Problem am besten ist.

Klassen erweitern

Am Ende des letzten Kapitels haben Sie eine Methode geschrieben, um eine Zahl als Wort zurückzugeben. Das war jedoch keine Integer-Methode: Es war nur eine generische Programmmethode. Wäre es nicht cooler, wenn Sie 22.als_wort anstatt deutscheNummer 22 schreiben könnten? So können Sie das machen:

class Integer
  def als_wort
    if self == 5
      wort = 'fünf'
    else
      wort = 'achtundfünfzig'
    end

    wort
  end
end

# Ich teste es lieber mal...
puts 5.als_wort
puts 58.als_wort

Nun, ich habe es getestet; und nichts ist explodiert. :)

Wir haben eine Integer-Methode definiert, indem wir einfach in die Integer-Klasse "gesprungen" sind, die Methode dort definiert haben und wieder herausgesprungen sind. Jetzt haben alle Integers diese tolle (unvollständige) Methode. Tatsächlich, wenn Ihnen die Art und Weise, wie die native to_s-Methode Dinge tut, nicht gefällt, können Sie sie einfach neu definieren... aber ich empfehle das nicht! Es ist am besten, die alten Methoden in Ruhe zu lassen und neue zu machen, wenn Sie etwas Neues brauchen.

Schon verwirrt? Lassen Sie mich das letzte Programm noch etwas genauer durchgehen. Bisher haben wir, wann immer wir Code ausgeführt oder eine Methode definiert haben, dies im standardmäßigen "Programm"-Objekt getan. In unserem letzten Programm haben wir dieses Objekt zum ersten Mal verlassen und sind in die Integer-Klasse gegangen. Wir haben dort eine Methode definiert (was sie zu einer Integer-Methode machte) und alle Integers können sie verwenden. Innerhalb dieser Methode verwenden wir self, um auf das Objekt (den Integer) zu verweisen, das die Methode verwendet.

Klassen erstellen

Wir haben schon viele Objekte verschiedener Klassen gesehen. Es ist jedoch einfach, Arten von Objekten zu erstellen, die Ruby nicht hat. Glücklicherweise ist das Erstellen einer neuen Klasse genauso einfach wie das Erweitern einer bestehenden. Nehmen wir an, wir wollten in Ruby würfeln. So könnten wir eine Klasse Wuerfel machen:

class Wuerfel

  def rollen
    1 + rand(6)
  end

end

# Lass uns ein paar Würfel machen...
wuerfel = [Wuerfel.new, Wuerfel.new]

# ...und sie rollen.
wuerfel.each do |w|
  puts w.rollen
end

(Wenn Sie den Abschnitt über Zufallszahlen übersprungen haben, rand(6) gibt einfach eine Zufallszahl zwischen 0 und 5 zurück).

Das ist es! Objekte unserer ganz eigenen Art. Rollen Sie die Würfel ein paar Mal (indem Sie den "Aktualisieren"-Button in Ihrem Browser drücken) und beobachten Sie, was passiert.

Wir können alle möglichen Methoden für unsere Objekte definieren... aber es fehlt etwas. Die Arbeit mit diesen Objekten hat sich nicht viel geändert, seit wir etwas über Variablen gelernt haben. Schauen Sie sich zum Beispiel unseren Würfel an. Jedes Mal, wenn wir ihn rollen, erhalten wir eine andere Zahl. Aber wenn wir diese Zahl speichern wollten, müssten wir eine Variable erstellen, die darauf zeigt. Und jeder anständige Würfel sollte eine Zahl haben, und das Rollen des Würfels sollte diese Zahl ändern. Wenn wir den Würfel behalten, haben wir keine Möglichkeit zu wissen, welche Zahl er gerade zeigt.

Wenn wir jedoch versuchen, die gewürfelte Zahl in einer (lokalen) Variable innerhalb von rollen zu speichern, ist sie weg, sobald rollen beendet ist. Wir müssen diese Zahl in einer anderen Art von Variable speichern:

Instanzvariablen

Normalerweise, wenn wir über Strings sprechen, nennen wir sie einfach Strings. Wir könnten sie jedoch String-Objekte nennen. Manchmal nennen Programmierer sie Instanzen der Klasse String, aber das ist nur eine schicke (und ziemlich lange) Art, String zu sagen. Eine Instanz einer Klasse ist einfach ein Objekt dieser Klasse.

Instanzvariablen sind also einfach Variablen eines Objekts. Die lokalen Variablen einer Methode bleiben bestehen, bis die Methode beendet ist. Die Instanzvariablen eines Objekts hingegen bleiben so lange bestehen, wie das Objekt existiert. Um Instanzvariablen von lokalen Variablen zu unterscheiden, haben sie ein @ vor ihren Namen:

class Wuerfel

  def rollen
    @zahlGezeigt = 1 + rand(6)
  end

  def gezeigt
    @zahlGezeigt
  end

end

wuerfel = Wuerfel.new
wuerfel.rollen
puts wuerfel.gezeigt
puts wuerfel.gezeigt
wuerfel.rollen
puts wuerfel.gezeigt
puts wuerfel.gezeigt

Sehr schön! Jetzt rollt rollen den Würfel und gezeigt sagt uns, welche Zahl gezeigt wird. Aber was ist, wenn wir versuchen zu sehen, was gezeigt wird, bevor wir den Würfel gerollt haben (bevor wir @zahlGezeigt definiert haben)?

class Wuerfel

  def rollen
    @zahlGezeigt = 1 + rand(6)
  end

  def gezeigt
    @zahlGezeigt
  end

end

# Da ich diesen Würfel nicht mehr verwenden werde,
# muss ich ihn nicht in einer Variable speichern.
puts Wuerfel.new.gezeigt

Hmmm... Nun, zumindest gab es keinen Fehler. Trotzdem macht es nicht viel Sinn, dass ein "neuer" Würfel nil ist. Es wäre schön, wenn wir den Würfel direkt bei seiner Erstellung einrichten könnten. Dafür ist initialize da:

class Wuerfel

  def initialize
    # Ich werde den Würfel einfach rollen, obwohl wir
    # auch etwas anderes tun könnten, wenn wir wollten,
    # wie zum Beispiel den Würfel so einstellen, dass er 6 zeigt.
    rollen
  end

  def rollen
    @zahlGezeigt = 1 + rand(6)
  end

  def gezeigt
    @zahlGezeigt
  end

end

puts Wuerfel.new.gezeigt

Wenn ein Objekt erstellt wird, wird die initialize-Methode (falls definiert) immer aufgerufen.

Unser Würfel ist fast perfekt. Das einzige, was fehlt, ist eine Möglichkeit festzulegen, welche Seite gezeigt wird... Warum schreiben Sie nicht eine schummeln-Methode, die genau das tut? Kommen Sie zurück, wenn Sie fertig sind (und wenn Sie getestet haben, dass es funktioniert, natürlich). Stellen Sie sicher, dass niemand den Würfel dazu bringen kann, eine 7 zu zeigen!

Das war ziemlich cool. Aber es war immer noch ein Spielzeugbeispiel. Lassen Sie mich Ihnen ein interessanteres Beispiel zeigen. Lassen Sie uns ein virtuelles Haustier machen, einen Babydrachen. Wie die meisten Babys sollte er essen, schlafen und sein Geschäft machen können, was bedeutet, dass wir ihn füttern, ins Bett bringen und mit ihm Gassi gehen müssen. Intern muss unser Drache wissen, ob er hungrig oder müde ist oder mal muss, aber wir werden das nicht sehen können, wenn wir mit ihm interagieren, genau wie Sie ein menschliches Baby nicht fragen können: "Bist du hungrig?". Wir werden auch ein paar andere lustige Möglichkeiten hinzufügen, um mit unserem Babydrachen zu interagieren, und wenn er geboren wird, geben wir ihm einen Namen (Alles, was Sie an die new-Methode übergeben, wird an die initialize-Methode für Sie übergeben). Also gut, versuchen wir es:

class Drache

  def initialize name
    @name = name
    @schlafend = false
    @zeugImBauch     = 10  # Er ist voll.
    @zeugImDarm      =  0  # Er muss nicht.

    puts @name + ' ist geboren.'
  end

  def fuettern
    puts 'Du fuetterst ' + @name + '.'
    @zeugImBauch = 10
    zeitVergeht
  end

  def gassi
    puts 'Du gehst mit ' + @name + ' Gassi.'
    @zeugImDarm = 0
    zeitVergeht
  end

  def insBettBringen
    puts 'Du bringst ' + @name + ' ins Bett.'
    @schlafend = true
    3.times do
      if @schlafend
        zeitVergeht
      end
      if @schlafend
        puts @name + ' schnarcht und fuellt den Raum mit Rauch.'
      end
    end
    if @schlafend
      @schlafend = false
      puts @name + ' wacht langsam auf.'
    end
  end

  def werfen
    puts 'Du wirfst ' + @name + ' in die Luft.'
    puts 'Er kichert und versengt deine Augenbrauen.'
    zeitVergeht
  end

  def wiegen
    puts 'Du wiegst ' + @name + ' sanft.'
    @schlafend = true
    puts 'Er does kurz weg...'
    zeitVergeht
    if @schlafend
      @schlafend = false
      puts '...aber wacht auf, als du aufhoerst.'
    end
  end

  private

  # "private" bedeutet, dass die hier definierten Methoden
  # interne Methoden des Objekts sind. (Du kannst
  # deinen Drachen fuettern, aber du kannst ihn nicht fragen, ob er hungrig ist.)

  def hungrig?
    # Methodennamen können mit "?" enden.
    # Normalerweise tun wir dies nur, wenn die Methode
    # true oder false zurückgibt, wie hier:
    @zeugImBauch <= 2
  end

  def mussMal?
    @zeugImDarm >= 8
  end

  def zeitVergeht
    if @zeugImBauch > 0
      # Essen vom Bauch in den Darm bewegen.
      @zeugImBauch     = @zeugImBauch     - 1
      @zeugImDarm      = @zeugImDarm + 1
    else  # Unser Drache verhungert!
      if @schlafend
        @schlafend = false
        puts 'Er wacht ploetzlich auf!'
      end
      puts @name + ' verhungert! In Verzweiflung hat er DICH gefressen!'
      exit  # Das beendet das Programm.
    end

    if @zeugImDarm >= 10
      @zeugImDarm = 0
      puts 'Hoppla! ' + @name + ' hatte einen Unfall...'
    end

    if hungrig?
      if @schlafend
        @schlafend = false
        puts 'Er wacht ploetzlich auf!'
      end
      puts @name + 's Magen knurrt...'
    end

    if mussMal?
      if @schlafend
        @schlafend = false
        puts 'Er wacht ploetzlich auf!'
      end
      puts @name + ' macht den Toepfchentanz...'
    end
  end

end

haustier = Drache.new 'Norbert'
haustier.fuettern
haustier.werfen
haustier.gassi
haustier.insBettBringen
haustier.wiegen
haustier.insBettBringen
haustier.insBettBringen
haustier.insBettBringen
haustier.insBettBringen

WOW! Natürlich wäre es viel schöner, wenn dies ein interaktives Programm wäre, aber das können Sie später machen. Ich wollte Ihnen nur die Teile zeigen, die direkt mit der Erstellung einer neuen Drache-Klasse zusammenhängen.

Wir haben in diesem Beispiel ein paar neue Dinge gesehen. Das erste ist einfach: exit beendet das Programm sofort. Das zweite ist das Wort private, das wir mitten in unsere Klassendefinition gesteckt haben. Ich hätte es weglassen können, aber ich wollte die Idee durchsetzen, dass bestimmte Methoden Dinge sind, die man einem Drachen antun kann, und andere Methoden Dinge sind, die einfach innerhalb des Drachen passieren. Sie können sich diese als "unter der Haube"-Dinge vorstellen: Wenn Sie kein Automechaniker sind, müssen Sie eigentlich nur wissen, wie man das Gaspedal, das Bremspedal und das Lenkrad benutzt. Ein Programmierer würde dies die öffentliche Schnittstelle nennen.

Nun zu einem konkreteren Beispiel in dieser Richtung, lassen Sie uns darüber sprechen, wie Sie ein Auto in einem Videospiel darstellen könnten (was mein Arbeitsgebiet ist). Zuerst müssen Sie entscheiden, wie Ihre öffentliche Schnittstelle aussieht; mit anderen Worten, welche Methoden sollten Leute auf Ihren Auto-Objekten aufrufen können? Nun, sie müssen das Gaspedal und das Bremspedal drücken können, aber sie müssten auch angeben können, wie stark sie das Pedal drücken (Es gibt einen großen Unterschied zwischen Durchtreten und Antippen). Sie müssten auch lenken können, und wieder sagen, wie stark sie das Lenkrad drehen. Ich nehme an, Sie könnten weiter gehen und eine Kupplung, Blinker, Raketenwerfer, Heckverbrennungsanlage, Fluxkompensator usw. hinzufügen... je nachdem, welche Art von Spiel Sie machen.

Innerhalb eines Auto-Objekts sind die Dinge jedoch viel komplexer: Andere Dinge, die ein Auto bräuchte, sind eine Geschwindigkeit, eine Richtung und eine Position (im einfachsten Sinne). Diese Attribute würden natürlich durch Drücken der Gas- oder Bremspedale und Drehen des Lenkrads geändert, aber der Benutzer sollte nicht in der Lage sein, die Position des Autos direkt festzulegen (das wäre wie Beamen). Sie könnten auch Schleudern oder Schaden, Luftwiderstand und so weiter verfolgen wollen. All diese Dinge sind intern für das Auto.

Ein paar Dinge zum Ausprobieren

  • Erstellen Sie eine Orangenbaum-Klasse. Sie sollte eine hoehe-Methode haben, die ihre Höhe zurückgibt, und eine einJahrVergeht-Methode, die, wenn sie aufgerufen wird, den Baum um ein Jahr altern lässt. Jedes Jahr wird der Baum größer (wie viel auch immer Sie denken, dass ein Orangenbaum in einem Jahr wachsen sollte), und nach einer gewissen Anzahl von Jahren (wieder Ihre Entscheidung) sollte der Baum sterben. In den ersten paar Jahren sollte er keine Früchte tragen, aber nach einer Weile schon, und ich schätze, dass ältere Bäume mit den Jahren viel mehr Früchte tragen... was auch immer Sie für am sinnvollsten halten. Und natürlich sollten Sie in der Lage sein, orangenZaehlen (was die Anzahl der Orangen am Baum zurückgibt) und orangePfluecken (was die @orangenAnzahl um eins reduziert und einen String zurückgibt, der Ihnen sagt, wie lecker die Orange war, oder Ihnen sagt, dass es dieses Jahr keine Orangen mehr zu pflücken gibt) zu können. Stellen Sie sicher, dass alle Orangen, die Sie in einem Jahr nicht pflücken, vor dem nächsten Jahr abfallen.
  • Schreiben Sie ein Programm, damit Sie mit Ihrem Babydrachen interagieren können. Sie sollten Befehle wie fuettern und gassi eingeben können, und diese Methoden sollten auf Ihrem Drachen aufgerufen werden. Da das, was Sie eingeben, Strings sind, müssen Sie natürlich eine Art Methodenverteilung haben, bei der Ihr Programm prüft, welcher String eingegeben wurde, und die entsprechende Methode aufruft.

Und das war's! Aber warten Sie... Ich habe Ihnen nichts über Klassen erzählt, die Dinge tun wie E-Mails senden oder Dateien auf Ihrem Computer speichern und laden, oder wie man Fenster und Knöpfe erstellt, oder 3D-Welten oder irgendetwas! Nun, es gibt einfach so viele Klassen, die Sie verwenden können, dass ich Ihnen unmöglich alle zeigen kann; ich kenne nicht einmal alle. Was ich Ihnen sagen kann, ist, wo Sie mehr darüber erfahren können, damit Sie die nachschlagen können, die Sie verwenden möchten. Aber bevor ich Sie wegschicke, gibt es noch eine Funktion von Ruby, die Sie wirklich kennen sollten, etwas, das die meisten anderen Sprachen nicht haben, aber ohne das ich einfach nicht leben kann: Blöcke und Procs.