Schleifen binden - Theorie Lektion 8

Erstellt von Frithjof Fri, 25 Jul 2008 20:58:00 GMT

Der letzte Artikel versprach vollmundig, dass es weitergeht. Und das wird es auch! Was sind schon 50 Tage? Es lebt sich auch gut ohne Ruby.

Zur Einstimmung nach dieser langen Pause gibt es nicht die bereits erwartete Abschlußlektion 20, sondern eine kleine Theorielektion zum Thema Rekursion.

Was ist Rekursion?

Rekursion leitet sich vom lateinischen Verb recurrere ab. Es bedeutet etwa zurückkehren, zurücklaufen, wobei im Zusammenhang mit Programmiersprachen wohl zurückkehren die treffendste scheint.

Mit Rekursion kann man wiederkehrende Programmabläufe gestalten ohne eine richtige Schleife zu verwenden. Mit Ruby hattest du bisher folgende Möglichkeiten kennengelernt, eine Schleife zu nutzen:

  • while
  • for each
  • Iteratoren

Jede Programmiersprache hat für Schleifen gewöhnlich spezielle Schlüsselwörter, die am Beginn oder Ende eines Programmabschnittes stehen, der solange wiederholt werden soll, bis eine gewisse Bedingung erfüllt ist, die dann zum Abbruch der Schleifendurchläufe führt.

Man kann aber alles, was man mit diesen speziellen Schlüsselwörtern als Schleifen programmieren kann auch ohne sie bewerkstelligen. Ja es gibt sogar (Programmier-) Sprachen, die für Schleifen überhaupt keine Schlüsselwörter bereitstellen. XSLT zum Beispiel bietet zwar ein Schlüsselwort for-each an, um über einer Eingabemenge eine Schleife zu bilden, allerdings ist man dabei direkt an die Länge der Eingabemenge gebunden. Und wenn keine Eingabemenge vorliegt, nützt dieses Schlüsselwort nicht viel. In XSLT kommt man somit um Rekursion überhaupt nicht herum, wenn man Schleifen braucht.

Grundprinzip der Rekursion

Ein erstes Merkmal der Rekursion ist, dass sich ein Programmabschnitt selbst erneut aufruft. Der erste Aufruf muss jedoch von außen erfolgen.

Irgendwann sollte das sich selber aufrufen aber zu Ende sein. Ein zweites Merkmal ist somit eine exakt definierte Abbruchbedinung. Ist diese Bedingung erfüllt, soll der Sich-Selber-Aufruf nicht mehr vorgenommen werden.

Ein einfaches Beispiel soll das verdeutlichen. Wir schreiben ein Rubyprogramm, dass sich selber aufruft. Dazu verwendet es die Methode system aus dem Rubykern. Mit system kann man Befehle der Kommandozeile aus einem Rubyprogramm heraus ausführen. Den Namen der Datei des aktuell laufenden Rubyprogramms erhältst du mit der globalen Variable __FILE__.


# theorie_08.rb
puts "Ein Hallo aus der Rekursion!" 

system("ruby " + __FILE__)

Starte das Programm an der Konsole mit


ruby theorie_08.rb

und es wird dir ewig den selben Gruß senden


[25.07.2008, 22:07]:> ruby theorie_08.rb
Ein Hallo aus der Rekursion!
Ein Hallo aus der Rekursion!
Ein Hallo aus der Rekursion!
Ein Hallo aus der Rekursion!
Ein Hallo aus der Rekursion!
...

solange bis du es endlich mit der Abbruchbedinung Strg-C abbrichst. Das ist eine ziemlich harte Abbruchbedingung.

Du bemerkst hier das Dilemma: das Programm selber kann nicht entscheiden, das wievielte mal es bereits aufgerufen wird. Es hat also nichts, woran es die Notwendigkeit eines Abbruchs erkennen könnte.

Das Programm muss beim Aufruf von sich selbst sich selbst einen Parameter mitgeben, mit dem irgendwie dann ein Abbruchkriterium formuliert werden kann.

Einem Rubyprogramm kann man einen Parameten mitgeben, indem man ihn einfach nach dem Programmnamen mit auf der Kommandozeile hinschreibt. Alle so nach dem Programmnamen angegebenen Parameter werden in einem globalen Array mit dem festen Namen ARGV festgehalten und stehen so innerhalb des Programms zur Verfügung. Merke dir die ARGV als ARGumentVektor (Vektor meint dasselbe wie Array oder Liste).


# sag_hallo.rb
if not ARGV[0].nil?
  puts "Hallo " + ARGV[0] + "!" 
else
  puts "Hallo! Wie ist dein Name?" 
end

[25.07.2008, 22:16]:> ruby sag_hallo.rb
Hallo! Wie ist dein Name?

[25.07.2008, 22:17]:> ruby sag_hallo.rb Livia
Hallo Livia!

Für das rekursive Programm kannst du nun prima die Parameterliste ARGV verwenden, um dem Programm ab dem ersten Aufruf über alle weiteren Selbstaufrufe hinweg Informationen zukommen zu lassen, die es dann für ein sinnvolles Abbruchkriterium verwenden kann.

Versuche es!


# theorie_08.rb

# Zuerst das Abbruchkriterium checken
parameter = ARGV[0]
if parameter.nil?
  # Ohne Parameter mach ich gar nix
  puts "Parameter fehlt!" 
  exit
else
  parameter = parameter.to_i - 1
end

# Abbrechen, wenn parameter den Wert 0 unterschreitet
if parameter < 0
  exit
end

# Ansonsten dann die eigentliche Arbeit verrichten
puts "Das #{parameter}. Hallo aus der Rekursion!" 

# Und schließlich sich selbst aufrufen
system("ruby " + __FILE__ + " " + parameter.to_s)

Mit exit wird das laufende Rubyprogramm sofort verlassen.

Wie du siehst, musst du hier etwas aufpassen wegen des Datentyps. Alle Parameter, die auf der Kommandozeile einem Programm übergeben werden, landen als Strings in der ARGV Liste. Zum herunterzählen musst du den Wert zunächst mit to_i in eine ganze Zahl (Integer) verwandeln und zum Anhängen an den Selbstaufruf erneut in einen String mit to_s konvertieren.

Startest du jetzt das Programm an der Konsole, zunächst ohne, dann mit Parameter,


[25.07.2008, 22:31]:> ruby theorie_08.rb
Parameter fehlt!

[25.07.2008, 22:31]:> ruby theorie_08.rb 10
Das 9. Hallo aus der Rekursion!
Das 8. Hallo aus der Rekursion!
Das 7. Hallo aus der Rekursion!
Das 6. Hallo aus der Rekursion!
Das 5. Hallo aus der Rekursion!
Das 4. Hallo aus der Rekursion!
Das 3. Hallo aus der Rekursion!
Das 2. Hallo aus der Rekursion!
Das 1. Hallo aus der Rekursion!
Das 0. Hallo aus der Rekursion!

dann bricht die Selbstaufrufserie wie gewünscht ab, sobald der übergebene Parameter auf weniger als 0 heruntergezählt wurde.

Rekursion innerhalb eines Programms

Wie du an dem hin und herkonvertieren gesehen hast, ist die Verwendung der Rekursion auf das gesamte Programm recht fehleranfällig. Diese Art wird dir sicher auch recht selten begegnen. Viel natürlicher ist es, innerhalb eines Programms eine Rekursion aufzubauen. Welche Möglichkeiten hast du denn, um in einem Programm etwas aufzurufen?

Na klar, Methoden kann man aufrufen. Wir schreiben das bisherige Programm so um, dass wir den Teil, der sich wiederholen soll in einer eigenen Methode steht.

Zuerst musst du diese Methode einmal aufrufen (ähnlich dem ersten Programmaufruf an der Kommandozeile) und alles weitere besorgt die Methode selbst. Sie prüft das Abbruchkriterium und je nach dem arbeitet sie was und ruft sich selber nochmal auf, oder sie bricht ab.


# theorie_08_b.rb

def sag_was(counter)
  if counter.nil?
    # Ohne Parameter mach ich gar nix
    puts "Parameter fehlt!" 
    return
  else
    counter = counter - 1
  end

  # Abbrechen, wenn counter den Wert 0 unterschreitet
  if counter < 0
    return
  end

  # Ansonsten dann die eigentliche Arbeit verrichten
  puts "Das #{counter}. Hallo aus der Rekursion!" 

  # Und schließlich sich selbst aufrufen
  sag_was(counter)
end

# Erster Aufruf immer von außen
sag_was(10)

[25.07.2008, 22:31]:> ruby theorie_08_b.rb
Das 9. Hallo aus der Rekursion!
Das 8. Hallo aus der Rekursion!
Das 7. Hallo aus der Rekursion!
Das 6. Hallo aus der Rekursion!
Das 5. Hallo aus der Rekursion!
Das 4. Hallo aus der Rekursion!
Das 3. Hallo aus der Rekursion!
Das 2. Hallo aus der Rekursion!
Das 1. Hallo aus der Rekursion!
Das 0. Hallo aus der Rekursion!

Du bemerkst sofort, dass das umgeschriebene Programm viel schneller läuft. Das liegt daran, dass hier nur einmal der Rubyinterpreter gestartet werden musss. Alle rekursiven Aufrufe laufen in einer Instanz des Interpreters ab.

Voher wurde ja rekursiv immer eine neue Instanz des Rubyinterpreters gestartet— das dauerte etwas länger und verschwendete Systemressourcen.

Zu guter Letzt noch ein paar Schönheitskorrekturen: die Zählung in richtiger Reihenfolge ausgeben und ARGV nutzen, um beim Aufruf des Programms einen Startwert festlegen zu können:


# theorie_08_b.rb

def sag_was(counter, start = nil)
  if start.nil? then start = counter end
  if counter.nil?
    # Ohne Parameter mach ich gar nix
    puts "Parameter fehlt!" 
    return
  else
    counter = counter - 1
  end

  # Abbrechen, wenn counter den Wert 0 unterschreitet
  if counter < 0
    return
  end

  # Ansonsten dann die eigentliche Arbeit verrichten
  puts "Das #{start - (counter % start)}. Hallo aus der Rekursion!" 

  # Und schließlich sich selbst aufrufen
  sag_was(counter, start)
end

# Erster Aufruf immer von außen
sag_was(ARGV[0].to_i)

[25.07.2008, 22:42]:> ruby theorie_08_b.rb 5
Das 1. Hallo aus der Rekursion!
Das 2. Hallo aus der Rekursion!
Das 3. Hallo aus der Rekursion!
Das 4. Hallo aus der Rekursion!
Das 5. Hallo aus der Rekursion!

Wann Rekursion und wann lieber nicht?

Vermeide Rekursion, wo es nur möglich ist. Auch wenn Rekursion recht elegant daher kommt, büßt das Programm etwas an Lesbarkeit ein. Außerdem ist es schwieriger, einen Fehler in einem rekursiven Programm zu finden, als in gewöhnlichen Schleifen.

Nicht selten ist aber ein rekursiver Ansatz vorzuziehen. Insbesondere, wenn du einen Algorithmus implementieren musst, der bereits rekursiv entworfen ist, oder wenn sich die Rekursivität geradezu aufdrängt. Dann kann es sogar manchmal viel schwieriger sein, eine rekursionsfreie Implementierung zu finden.

Um Rekursion kommst du sowieso nicht herum, wenn die (Programmier-) Sprache keine Befehle für Schleifen anbietet (siehe XSLT). Spätestens dann musst du dich mit Rekursion vertraut machen.

- Änderungen
28.07.2008, Bei Rekursion mit Methode reicht return anstatt exit.

OOP vertieft - Theorie Lektion 7

Erstellt von Frithjof Mon, 24 Dec 2007 12:40:00 GMT

In Lektion 16 hast du einen ersten Eindruck von objektorientierter Programmierung (abgekürzt OOP) bekommen. Die prozedurale Programmierung im Spiel Tic-Tac-Toe führte dazu, dass wir eine Methode nach der anderen implementierten, die aber alle irgendwie zusammengehören. In Lektion 17, der Abschlußlektion des Spiels, wollen wir gemeinsam den bisherigen Rubycode für Tic-Tac-Toe in einen objektorientierten Code umschreiben.

Dazwischen füge ich diese Theorielektion ein, damit wir etwas Zeit haben uns mit dem Neuen vertraut zu machen.

Du programmierst ein Wohnhaus auf objektorientierte Art und Weise. Formulieren wir zunächst etwas vereinfacht, was ein Wohnhaus überhaupt ist.

Ein Wohnhaus ist ein Gebäude mit einer Haustür und mehreren Fenstern, einem Dach mit Schornstein. Innen hat das Wohnhaus mehrere Zimmer und Möbel, einen Kachelofen. Die Tür und die Fenster kann man öffnen und schließen, der Schornstein raucht, solange der Kachelofen angeschürt ist.

Diesen beschreibenden Text eines Wohnhauses gilt es nun in ein objektorientiertes Programm umzuwandeln. Du musst irgendwie herausfinden, welche Objekte hier im Spiel sind, damit du die entsprechenden Klassen implementieren kannst und welche Methoden diese Objekte haben sollen. Dafür gibt es eine klassische Vorgehensweise:

  1. Markiere alle Substantive im Text, das sind die Klassen!
    Ein Wohnhaus ist ein Gebäude mit einer Haustür und mehreren Fenstern, einem Dach mit Schornstein. Innen hat das Wohnhaus mehrere Zimmer und Möbel, einen Kachelofen.

    Die Tür und die Fenster kann man öffnen und schließen, der Schornstein raucht, solange der Kachelofen angeschürt ist.

  2. Markiere alle Verben im Text, das sind die Methoden!
    Ein Wohnhaus ist ein Gebäude mit einer Haustür und mehreren Fenstern, einem Dach mit Schornstein. Innen hat das Wohnhaus mehrere Zimmer und Möbel, einen Kachelofen.

    Die Tür und die Fenster kann man öffnen und schließen, der Schornstein raucht, solange der Kachelofen angeschürt ist.


  3. Das Programm funktioniert dann so als würden die Substantive die Verben anderer Substantive aufrufen.

Wir werden nicht alle Klassen implementieren, sondern nur folgende:

  • Tür
  • Fenster
  • Kachelofen
  • Wohnhaus

Fangen wir mit der Klasse Tür an.


class Tuer
  def initialize(farbe, material)
    @farbe = farbe
    @material = material
    @zustand = :geschlossen
  end
end

Im Konstruktor initialize werden drei Attribute der Klasse (das sind die Variablen mit dem @-Zeichen) angelegt: farbe, material und zustand. Wozu das Attribut farbe dient ist ziemlich klar. Im Attribut material wird gespeichert, aus welchem Material die Tür besteht (Holz, Metall, ...) und im Attribut zustand, ob sie offen oder geschlossen ist.

Der Konstruktor bekommt von außen die gewünschte Farbe und das Material übergeben. Der Zustand wird nicht übergeben, sondern immer auf :geschlossen gesetzt. Statt :geschlossen könnten wir auch "geschlossen" verwenden. Wir müssen uns nur für eine Schreibweise entscheiden und diese dann später beim Ändern des Zustandes beibehalten.

Die Klasse Tuer funktioniert schon, wie das folgende Beispiel zeigt. Es werden zwei Objekte der Klasse Tuer angelegt.

  tuer1 = Tuer.new("schwarz", :holz)
  tuer2 = Tuer.new("blau", :metall)

  puts tuer1
  puts tuer2
Lassen wir das Programm laufen, liefert es folgendes:

C:\entwicklung>ruby theorie_07.rb
#<Tuer:0x2c7f9b4>
#<Tuer:0x2c7f98c>

Naja, sieht ja nicht gerade informativ aus. Schöner wäre doch, wenn der puts Befehl auch die Inhalte der Attribute ausgeben würde. Das geht so einfach nicht, weil der puts Befehl nichts von den Attributen einer Tür weiß. Es gibt aber eine Möglichkeit: der puts Befehl versucht immer eine Methode to_s (englisch abgekürzt für to string) am Objekt aufzurufen. Wird sie gefunden, dann wird der Rückgabewert dieser Methode ausgegeben, andernfalls macht puts was es will. Eine schöne to_s Methode wäre vielleicht die folgende:


class Tuer
  def initialize(farbe, material)
    @farbe = farbe
    @material = material
    @zustand = :geschlossen
  end

  def to_s
    "Tuer[zustand=#{@zustand}, farbe=#{@farbe}, material=#{@material}]" 
  end
end
Damit sieht die Ausgabe gleich viel besser aus:

C:\entwicklung>ruby theorie_07.rb
Tuer[zustand=geschlossen, farbe=schwarz, material=holz]
Tuer[zustand=geschlossen, farbe=blau, material=metall]

Ganz zufrieden sind wir aber immer noch nicht. Oft werden wir auf ein einzelnes Attribut zugreifen wollen. Innerhalb der Klasse können wir das ja immer über den Variablennamen mit dem @-Zeichen machen. Aber von außen geht das nicht (Stichwort Kapselung, du erinnerst dich?).


  tuer1 = Tuer.new("schwarz", :holz)
  puts tuer1.farbe

Versuchen wir es, erhalten wir einen NoMethodError Fehler.


C:\entwicklung>ruby theorie_07.rb
theorie_07.rb: undefined method `farbe' for #<Tuer:0x2c7f838> (NoMethodError)

Es braucht also noch eine Methode, die uns den Inhalt des Attributes nach außen liefert:


class Tuer
  def initialize(farbe, material)
    @farbe = farbe
    @material = material
    @zustand = :geschlossen
  end

  def farbe
    @farbe
  end

  def to_s
    "Tuer[zustand=#{@zustand}, farbe=#{@farbe}, material=#{@material}]" 
  end
end

Dann funktioniert der Aufruf von puts tuer1.farbe, aber irgendwie unschön ist das doch, für jedes Attribut so eine Methode anbieten zu müssen. Ruby hält dafür eine Abkürzung bereit:


class Tuer
  attr_reader :farbe, :material, :zustand
  attr_writer                    :zustand

  def initialize(farbe, material)
    @farbe = farbe
    @material = material
    @zustand = :geschlossen
  end

  def to_s
    "Tuer[zustand=#{@zustand}, farbe=#{@farbe}, material=#{@material}]" 
  end
end

Mit attr_reader legen wir fest, welche Attribute von außen gelesen werden dürfen. Mit attr_writer legen wir fest, welche Attribute von außen beschrieben werden dürfen. Oben haben wir festgelegt, dass nur der Zustand gelesen und beschrieben, also geändert werden darf. Die Attribute farbe und material sind unveränderbar. Sie behalten den Wert, den sie beim Konstruktoraufruf (Methode initialize) erhalten haben. Das sind readonly (engl. nur lesen) Attribute.

Wir geben uns mit der Klasse zunächst einmal zufrieden und machen mit der Klasse Fenster weiter. Die ist ziemlich ähnlich aufgebaut:


class Fenster
  attr_reader :farbe, :material, :zustand
  attr_writer                    :zustand

  def initialize(farbe, material)
    @farbe = farbe
    @material = material
    @zustand = :geschlossen
  end

  def to_s
    "Fenster[zustand=#{@zustand}, farbe=#{@farbe}, material=#{@material}]" 
  end
end

Womit wir auch schon bei der Klasse Kachelofen angelangt wären, die sich auch nur wenig von den bisherigen unterscheidet:


class Kachelofen
  attr_reader :farbe, :zustand
  attr_writer         :zustand

  def initialize(farbe = "gelb")
    @farbe = farbe
    @zustand = :aus
  end

  def to_s
    "Kachelofen[zustand=#{@zustand}, farbe=#{@farbe}]" 
  end
end

Beim Kachelofen ist aber etwas neu. Der Konstruktor bekommt genau einen Parameter übergeben: farbe. Der Parameterwert wird aber dabei auf den Wert “gelb” gesetzt. Das ist ein sogenannter Default, also ein Standardwert. Falls beim Anlegen des Objektes keine Farbe für den Kachelofen festgelegt wird, dann ist er immer gelb.


  k1 = Kachelofen.new
  puts k1

  k2 = Kachelofen.new("braun")
  puts k2

C:\entwicklung>ruby theorie_07.rb
Kachelofen[zustand=aus, farbe=gelb]
Kachelofen[zustand=aus, farbe=braun]

Kommen wir zur letzten Klasse Wohnhaus. Jetzt wird es erst wirklich interessant. Ein Objekt der Klasse Wohnhaus verwaltet all die anderen Objekte Tuer, Fenster, Kachelofen.


class Wohnhaus
  attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster

  def initialize(farbe, farbe_dach)
    @farbe      = farbe
    @farbe_dach = farbe_dach
    @kachelofen = Kachelofen.new
    @tuer       = Tuer.new("schwarz", :holz)
    f = Fenster.new("braun", :holz)
    @fenster    = [f, f, f]
  end

  def to_s
    res = "Wohnhaus[\n" 
    res << "  farbe       = " + @farbe + "\n" 
    res << "  dach        = " + @farbe_dach + "\n" 
    res << "  kachelofen  = " + @kachelofen.to_s + "\n" 
    res << "  tuer        = " + @tuer.to_s + "\n" 
    res << "  schornstein = " + @schornstein.to_s + "\n" 
    res << "  fenster     = " + "\n" 
    for f in @fenster
      res << "    " + f.to_s + "\n" 
    end
    res << "\n" 
    res << "]" 

    res
  end
end

Ein Objekt der Klasse Wohnhaus hat zu Beginn (d.h. so wie es vom Konstruktor initialize ausgeliefert wird) eine bestimmte @farbe, das Dach hat eine vielleicht andere @farbe_dach. Es besitzt einen @kachelofen, genau eine @tuer und drei @fenster.

Die to_s Methode ist etwas umfangreicher, weil wir hier viele Details des Hauses ausgeben möchten. Wir nutzen dabei die to_s Methoden der jeweiligen Objekte.

Auch hier ist wieder eine Kleinigkeit neu. Den Zugriff auf die Attribute der Klasse Wohnhaus legen wir diesmal in der Zeile


  ...
  attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
  ...

fest. attr_accessor bedeutet, dass wir alle so festgelegten Attribute sowohl lesen als auch beschreiben können. Alternativ hätten wir auch ausführlich es so festlegen können:


  ...
  attr_reader :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
  attr_writer :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
  ...

Das Haus verändern

In der Beschreibung eines Wohnhauses haben wir gesagt, dass man die Tür und die Fenster öffnen und schließen kann und der Schornstein raucht, solange der Kachelofen angeschürt ist.

Fenster und Türen öffnen und schließen

Fenster und Türen haben einen Zustand, den wir im Konstruktor immer mit :geschlossen festgelegt haben. Wollen wir eine Tür oder eine Fenster öffnen, muss dieser Zustand den Wert :offen erhalten. Wir brauchen dafür eine Methode, die wir von außen aufrufen, und die dann den Zustand der Tür oder der Fenster ändert.

Das Öffnen und Schließen der Tür übernehmen die Methoden tuer_auf und tuer_zu.


class Wohnhaus
  attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
  ...
  def tuer_auf
    @tuer.zustand = :offen
  end

  def tuer_zu
    @tuer.zustand = :geschlossen
  end
  ...
end

Bei den Fenster ist etwas mehr zu tun, weil es nicht nur ein Fenster gibt.


class Wohnhaus
  attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
  ...
  def fenster_auf
    for f in @fenster
      f.zustand = :offen
    end
  end

  def fenster_zu
    for f in @fenster
      f.zustand = :geschlossen
    end
  end
  ...
end

Den Kachelofen anschüren

Auch der Kachelofen hat eine Zustand, der anzeigt, ob er aus oder angeschürt ist. Das Ändern dieses Zustandes übernehmen die beiden Methoden ofen_an und ofen_aus.


class Wohnhaus
  attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
  ...
  def ofen_an
    @kachelofen.zustand = :an
    @schornstein        = :raucht
  end

  def ofen_aus
    @kachelofen.zustand = :aus
    @schornstein        = :aus
  end
  ...
end

Die Methoden ofen_an bzw. ofen_aus überwachen also sowohl den Zustand des Kachelofens als auch des Schornsteins. So können wir sicher stellen, dass beide Zustände immer gleichzeitig in übereinstimmender Weise geändert werden. Es kann so nicht passieren, dass der Ofen aus geht, aber der Schornstein noch weiter raucht. Oder doch?

Hier nochmal alle Klassen zusammen. Ganz unten ein paar Zeilen Code, die ein Wohnhausobjekt anlegen und es verändern.


class Tuer
  attr_reader :farbe, :material, :zustand
  attr_writer                    :zustand

  def initialize(farbe, material)
    @farbe = farbe
    @material = material
    @zustand = :geschlossen
  end

  def to_s
    "Tuer[zustand=#{@zustand}, farbe=#{@farbe}, material=#{@material}]" 
  end
end

class Fenster
  attr_reader :farbe, :material, :zustand
  attr_writer                    :zustand

  def initialize(farbe, material)
    @farbe = farbe
    @material = material
    @zustand = :geschlossen
  end

  def to_s
    "Fenster[zustand=#{@zustand}, farbe=#{@farbe}, material=#{@material}]" 
  end
end

class Kachelofen
  attr_reader :farbe, :zustand
  attr_writer         :zustand

  def initialize(farbe = "gelb")
    @farbe = farbe
    @zustand = :aus
  end

  def to_s
    "Kachelofen[zustand=#{@zustand}, farbe=#{@farbe}]" 
  end
end

class Wohnhaus
  attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster

  def initialize(farbe, farbe_dach)
    @farbe      = farbe
    @farbe_dach = farbe_dach
    @kachelofen = Kachelofen.new
    @schornstein = :aus
    @tuer       = Tuer.new("schwarz", :holz)
    f = Fenster.new("braun", :holz)
    @fenster    = [f, f, f]
  end

  def tuer_auf
    @tuer.zustand = :offen
  end

  def tuer_zu
    @tuer.zustand = :geschlossen
  end

  def fenster_auf
    for f in @fenster
      f.zustand = :offen
    end
  end

  def fenster_zu
    for f in @fenster
      f.zustand = :geschlossen
    end
  end

  def ofen_an
    @kachelofen.zustand = :an
    @schornstein        = :raucht
  end

  def ofen_aus
    @kachelofen.zustand = :aus
    @schornstein        = :aus
  end

  def to_s
    res = "Wohnhaus[\n" 
    res << "  farbe       = " + @farbe + "\n" 
    res << "  dach        = " + @farbe_dach + "\n" 
    res << "  kachelofen  = " + @kachelofen.to_s + "\n" 
    res << "  tuer        = " + @tuer.to_s + "\n" 
    res << "  schornstein = " + @schornstein.to_s + "\n" 
    res << "  fenster     = " + "\n" 
    for f in @fenster
      res << "    " + f.to_s + "\n" 
    end
    res << "\n" 
    res << "]" 

    res
  end
end

w = Wohnhaus.new("gelb", "rot")
w.tuer = Tuer.new("blau", :holz)
w.kachelofen = Kachelofen.new("braun")
# Ein weiteres Fenster einbauen
w.fenster << Fenster.new("weiss", :kunststoff)
puts w

w.tuer_auf
w.fenster_auf
puts w

w.ofen_an
w.tuer_zu
w.fenster_zu
puts w

Verletzliche Kapselung

Zurück zu obiger Frage: Kann es passieren, dass der Ofen aus geht, der Schornstein aber weiter raucht?


w = Wohnhaus.new("gelb", "rot")
w.ofen_an
w.kachelofen.zustand = :aus

puts w

Liefert folgende Ausgabe


C:\entwicklung>ruby theorie_07.rb
Wohnhaus[
  farbe       = gelb
  dach        = rot
  kachelofen  = Kachelofen[zustand=aus, farbe=gelb]
  tuer        = Tuer[zustand=geschlossen, farbe=schwarz, material=holz]
  schornstein = raucht
  fenster     =
    Fenster[zustand=geschlossen, farbe=braun, material=holz]
    Fenster[zustand=geschlossen, farbe=braun, material=holz]
    Fenster[zustand=geschlossen, farbe=braun, material=holz]

]

Der Ofen ist also aus, der Schornstein raucht aber noch. Wie konnte das passieren? Die Klasse Wohnhaus ist nicht sicher genug gekapselt. Sie lässt es zu, dass man direkt mit w.kachelofen auf den Kachelofen zugreifen kann. Dann kann man dort den Zustand ändern und das Wohnhaus bekommt von dieser Änderung nichts mit.

Wie können wir das Problem beheben und die Kapselung hier wieder sicher machen? Mindestens zwei Möglichkeiten bieten sich an.

  1. Wir lassen den direkten Zugriff auf das Attribut kachelofen nicht mehr zu:
    
    class Wohnhaus
      attr_accessor :farbe, :farbe_dach, :tuer, :fenster
      ...
    end
    
    Dann müssen wir dem Erschaffer des Hauses aber einen weiteren Parameter im Konstruktor anbieten, der es erlaubt, die Farbe des Ofens festzulegen.
  2. Wir belassen den Zugriff auf das Attribut kachelofen wie bisher, überschreiben jedoch die Lesemethode mit einer ausführlichen Methode. In dieser Methode übergeben wir nicht das Attribute @kachelofen nach außen zurück, sondern stets eine Kopie davon.
    
    class Wohnhaus
      attr_accessor :farbe, :farbe_dach, :kachelofen, :tuer, :fenster
      ...
      # Attribut kachelofen auslesen, dabei gegen Änderung schützen, indem
      # nur eine Kopie nach außen gegeben wird.
      def kachelofen
        Kachelofen.new(@kachelofen.farbe)
      end
      ...
    end
    
    Mit der Kopie kann der Aufrufen dann machen, was er will, das Attribut @kachelofen innerhalb der Klasse wird sich dadurch nicht ändern.

Üben mit Peter und Livia

Livia: Es ist aber trotzdem immer noch möglich, den Kachelofen aus zu machen und den Schornstein weiter rauchen zu lassen. Wie muss die Klasse Wohnhaus weiter angepasst werden, damit auch dieser Fehler behoben ist?

Peter: Füge für Schornstein noch eine eigene Klasse mit den notwendigen Attributen hinzu.

Liste oder Hash? - Theorie Lektion 6

Erstellt von Frithjof Sun, 02 Sep 2007 00:07:00 GMT

Die Ferien neigen sich dem Ende entgegegen und der letzte Artikel liegt auch schon etwas länger zurück. Es wird also Zeit für die letzte Theorielektion, bevor wir uns demnächst mit der Programmierung eines Spieles befassen werden.

Ich möchte noch einmal auf Listen und Hashes zu sprechen kommen, die wir bereits in der letzten Theorielektion 5 kennen gelernt hatten. Schauen wir uns hier einmal genauer an, wann es sinnvoll ist mit Listen zu arbeiten und wann ein Hash zu bevorzugen ist. Wir werden dazu folgendes untersuchen:

  1. Wie greift man auf die Elemente in einer Liste oder einem Hash zu?
  2. Wie stellt man fest, ob ein bestimmtes Element in der Liste enthalten ist oder nicht?
  3. Wie fügt man Element in eine Liste oder Hash hinzu oder löscht welche heraus?

Bildlich kannst du dir eine Liste wie eine Kette vorstellen, an der verschiedene Dinge aufgereiht werden. Die Liste


  liste = ["Auto", "Haus", "Baum", "Ball", "Buch", "Bett", "PC"]

würde dann etwa so aussehen:

Einen Hash stellst du dir hingegen vor wie die Kleiderhaken in deinem Kindergarten. Jeder Haken ist mit einem Bildchen versehen. Alle Kinder im Kindergarten bekommen je einen Kleiderhaken zugeordnet und erkennen ihren eigenen leicht an seinem Bildchen. Die folgenden Kleiderhaken

könntest du dann in Ruby vielleicht so ausdrücken:


kleiderhaken = {
  "Ball"   => "Susi",
  "Baum"   => "Livia",
  "Haus"   => "Petra",
  "Schiff" => "Rainer",
  "Auto"   => "Peter",
}

Jetzt, wo du dir beide Datenstrukturen ein wenig bildlich vorstellen kannst, zeige ich dir ein paar der wichtigsten Operationen. Eine Operation soll hier jede Veränderung, die man an oder mit der Datenstruktur vornimmt, bedeuten.

Zugriff auf Elemente

Angenommen wir haben folgende Liste und Hash:

kette = ["Auto", "Haus", "Baum"]
haken = {
  "Auto" => "Peter",
  "Haus" => "Petra",
  "Baum" => "Livia",
}

Wir wollen das zweite Element in der Liste haben. An dieser Stelle musst du dich in die Lage des Rubyinterpreters versetzen. Anders als du als Entwickler sieht er nicht, dass das zweite Element in der Liste “Haus” ist. Daher ist die Frage nach dem zweiten Element für dich zwar baby-einfach, aber für den Rubyinterpreter nicht. Ihm musst du genau sagen, an welcher Stelle in der Liste mit Namen kette er das Element zugreifen soll. Die Zahl, die du Ruby dafür angibst nennt man auch den Index der Liste. Tun wir das:


puts "Das zweite Element der Liste ist " + kette[1]

In Listen werden die Elemente ja beginnend mit 0 (Null) abgezählt. Daher ist das zweite Element eben nicht beim Index 2, sondern 1 abgelegt. Unterscheide also zwischen dem Index und der Position innerhalb der Liste. Der Index ist immer um Eins kleiner, als die Position an der das Element in der Liste steht.

Nun willst du auch im Hash mit dem Namen haken an das zweite Element heran. Schauen wir, was passiert, wenn wir das genauso machen:


puts "Das zweite Element im Hash ist " + haken[1]

Naja, so geht es wohl nicht. Ruby liefert den Wert nil. Das bedeutet, der Wert ist ungültig. Es liegt daran, dass die eckigen Klammern im Hash eine andere Bedeutung haben, als in einer Liste. In der Liste verwendest du sie für die Angabe des Index, im Hash hingegen für den Schlüssel (Key). Und im Hash haken gibt es den Key 1 nicht.

Es ist in Wirklichkeit so, dass es im Hash kein zweites Element gibt. Ein Hash hat keine festlegbare Reihenfolge wie eine Liste. Daher macht es bei einem Hash keinen Sinn, die Frage nach dem zweiten Element zu stellen. Die Frage müsste eher lauten: Gib mir den Wert zum Schlüssel soundso!.


puts "Der Wert zum Schlüssel 'Haus' im Hash ist " + haken["Haus"]

Suchen und Finden von Elementen

Mit dem Wissen, wie wir auf die einzelnen Elemente einer Liste und eines Hash zugreifen können, ist es nun gar nicht mehr so schwer, nach bestimmten Elementen in beiden Datenstrukturen zu suchen.

Das Suchen in der Liste könntest du etwa in folgender Methode ausdrücken:


def suche_in_liste_nach(wo, was)
  gefunden = false
  for element in wo do
    if element == was 
      gefunden = true
      break
    end
  end

  if gefunden
    puts "'" + was + "' ist drin!" 
  else
    puts "'" + was + "' ist NICHT drin!" 
  end
end

Suchst du nach dem Wert “Tisch”,


  suche_in_liste_nach(kette, "Tisch")

dann gibt die Methode aus, dass der gesuchte Wert (Variable was) nicht in der Liste wo enthalten ist. Beachte hier, dass die Variable kette, in der du die Liste definiert hast, an die Methode suche_in_liste_nach übergeben wird und innerhalb der Methode unter einem anderen Namen (nämlich wo) bekannt gemacht ist. Beide Variablen sind von einander abhängig, sie meinen also dieselbe Liste. Würde die Methode suche_in_liste_nach irgendetwas an der Liste wo verändern, würde die Liste kette davon auch berührt sein. Auch wenn die Variablennamen unterschiedlich sind, gibt es hier nur eine einzige Liste.

Hast du eine Idee, wie wir nach dem Wert Tisch in dem Hash suchen könnten? Bei einem Hash musst du dir zunächst klar machen, ob du nach einem Schlüssel suchst, oder nach einem Wert, wobei dir der Schlüssel dann ziemlich egal ist. Die Suche nach einem Schlüssel ist in einem Hash einfach. Es ist ausreichend, mit dem gesuchten Schlüssel einen Zugriff auf den möglichen Wert zu versuchen. Nur wenn der Wert nicht nil ist, können wir davon ausgehen, dass der Schlüssel wirklich existiert.


  suche_nach = "Tisch" 
  if haken[suche_nach] != nil
    puts "Den Schlüssel '" + suche_nach + "' gibt es!" 
  else
    puts "Den Schlüssel '" + suche_nach + "' gibt es nicht!" 
  end

Die Entscheidung also, ob ein gesuchter Key in einem Hash enthalten ist, ist ziemlich schnell durch einen einzigen Zugriff auf den Hash entscheidbar. Es ist diese Eigenschaft, die einen Hash so interessant macht und warum man sich am häufigsten für ihn entscheidet, wenn dieses Kriterium im Vordergrund steht. Bei einer Liste muss man notfalls immer alle Elemente anschauen, bevor man die Entscheidung entgültig treffen kann, bei einem Hash reicht ein Zugriff. Das spart in großen Programmen enorm Zeit.

Etwas mehr müssen wir tun, wenn wir den Schlüssel nicht kennen, aber wissen wollen, ob ein bestimmter Wert im Hash abgelegt ist. Gibt es im Hash haken für Livia einen Eintrag?


  suche_in_liste_nach(haken.values, "Livia")

Das sieht nur deswegen kürzer aus, weil wir hier unsere oben definierte Suchmethode für Listen wiederverwenden. Wie bitte? Liste? Wir suchen doch etwas in einem Hash, oder? Ich hatte dir in einer der letzten Lektionen bereits angedeutet, dass ein Hash im Prinzip aus zwei Listen besteht: einer Liste die alle Schlüssel als Elemente hat und einer zugehörigen Liste mit den jeweiligen Werten zu den einzelnen Schlüsseln. An die Liste aller Schlüssel kommst du mit der Anweisung


  haken.keys

heran, an die Liste der zugehörigen Werte mit


  haken.values

Und diese letztgenannte Liste durchsuchen wir nach dem gesuchten Wert Livia mit unserer oben definierten Suchmethode für Listen.

Hinzufügen und Löschen von Elementen

Verwende den Operator << zum Hinzufügen eines Elements in die Liste.


  kette << "Tisch" 
  suche_in_liste_nach(kette, "Tisch")

Lösche ein Element aus einer Liste, indem du an die Liste die Nachricht delete (engl. lösche!) schickst und in der Zusatzinformation der Nachricht den zu löschenden Wert angibst.


  kette.delete "Tisch" 
  suche_in_liste_nach(kette, "Tisch")

Füge einen neuen Wert in einen Hash ein, indem du einfach für den neuen Schlüssel einen Wert angibst. Beachte aber, dass wenn der Schlüssel vorher schon enthalten war, danach der neue Wert für diesen Schlüssel im Hash abgelegt wird.


  haken["Tisch"] = "Hans" 

Lösche ein Schlüssel-Wert Paar aus dem Hash ebenfalls mit der delete Nachricht. Die Zusatzinformation muss aber den Namen des Schlüssels angeben, nicht den des zugehörigen Wertes.


  haken.delete "Tisch" 

Ein komplizierteres Beispiel

Ich möchte dich aus dieser Lektion nicht ohne ein etwas komplizierteres Beispiel entlassen, mit dem ich dir zeigen möchte, dass die Entscheidung, ob man eine Liste oder einen Hash verwendet nicht immer ganz so leicht zu treffen ist.

Nicht selten hast du zunächst eine Liste, dann aber merkst du, dass du eigentlich zu jedem Element in der Liste einen weiteren Wert zuordnen möchtest. Klarer Fall, wir nehmen einen Hash, beispielsweise mit den Werten der Liste als Schlüssel. Das geht natürlich nur, wenn in der Liste alle Elemente untereinander verschieden sind, also in der Liste kein Wert doppelt vorkommt.

Stell dir vor, du möchtest dir mit Ruby für jeden Wochentag einer bestimmten Woche die Farbe des Pullovers deiner Erzieherin im Kindergarten merken (oder deiner Lehrerin, wenn du schon zur Schule gehst, oder deiner Chefin, falls das nicht mehr der Fall ist, oder …). Liste oder Hash? Das ist hier die Frage.

Pulloverfarbe mit zwei Listen

Fangen wir mit dem naheliegendsten an: wir nehmen zwei Listen. Eine für die Wochentage, und eine für die Farben an diesen Tagen, wobei die Reihenfolge hier in beiden Listen korrespondiert. Das soll heißen, dass erste Element ist der Montag in der Liste der Wochentage, also muss auch die erste Farbe in der Farbenliste die Pulloverfarbe für den Montag sein. Klar, oder? Geben wir auch gleich schon man alle korrespondierenden Werte nach der Definition unserer Listen mit aus.


  woche = ["Mo", "Di", "Mi", "Do", "Fr"]
  farbe = ["gelb", "blau", "pink", "rot", "grün"]

  tag_zaehler = 0
  for tag in woche do
    puts "Am " + tag + " ist die Farbe " + farbe[tag_zaehler]
    tag_zaehler = tag_zaehler + 1
  end

Du siehst, wir verwenden für beide Listen einen gemeinsamen Index mit dem Namen tag_zaehler, der uns die beiden Listen zusammenhält. Die Wochentage könnten somit auch in einer anderen als der üblichen Reihenfolge beginnend bei Montag bis Freitag in der Wochenliste stehen, solange die Farbwerte auch genauso in der Farbliste angeordnet würden.

Welche Farbe hatte ihr Pullover am Mittwoch? Wir suchen zunächst nach dem “Mi” in der Wochenliste, merken uns den Index und schauen dann mit diesem Index einmal in der Farbliste nach.


  woche = ["Mo", "Di", "Mi", "Do", "Fr"]
  farbe = ["gelb", "blau", "pink", "rot", "grün"]

  such_tag = "Mi" 
  tag_zaehler = 0
  for tag in woche do
    break if tag == such_tag
    tag_zaehler = tag_zaehler + 1
  end

  puts "Am " + such_tag + " ist die Farbe " + farbe[tag_zaehler] + "!" 

Pulloverfarbe mit einer Liste

Geht das ganze auch mit einer Liste? Warum nicht? Vielleicht so:

  pullover = ["Mo", "gelb", "Di", "blau", "Mi", "pink", "Do", "rot", "Fr", "grün"]

  tag_zaehler = 0
  1.upto(5) do
    tag   = pullover[tag_zaehler]
    farbe = pullover[tag_zaehler + 1]
    puts "Am " + tag + " ist die Farbe " + farbe
    tag_zaehler = tag_zaehler + 2
  end

Hintereinanderweg schreiben wir abwechselnd Tag und Farbe in eine gemeinsame Liste. Natürlich müssen wir nun mit dem Index etwas aufpassen. Der Index für die Farbe ist immer um genau Eins größer als der Index des Wochentages. Außerdem müssen wir am Ende unserer Schleife den Index für den Tag nicht nur um Eins, sondern gleich um Zwei erhöhen, um jeweils die Farbe zu überspringen und zum nächsten Tag zu gelangen.

Welche Farbe hatte ihr Pullover am Mittwoch? Wir suchen zunächst nach dem “Mi” in der gemeinsamen Liste, merken uns den Index und schauen dann mit dem um Eins erhöhten Index einmal in derselben Liste nach.


  pullover = ["Mo", "gelb", "Di", "blau", "Mi", "pink", "Do", "rot", "Fr", "grün"]

  such_tag = "Mi" 
  tag_zaehler = 0
  1.upto(5) do
    break if pullover[tag_zaehler] == such_tag
    tag_zaehler = tag_zaehler + 2
  end

  puts "Am " + such_tag + " ist die Farbe " + pullover[tag_zaehler + 1] + "!" 

Du siehst auch hier eine andere Art der Schleife. Upto bedeutet bis zu. Die Schleife läuft also von 1 bis zu 5, genau 5 mal durch. Wenn du dein Pulloverprotokoll für mehr als 5 Wochentage aufzeichnest, dann musst du die Zahlen hier entsprechend anpassen.

Pulloverfarbe mit vielen Listen in einer Liste

Du könntest natürlich auch die zusammengehörigen Werte in einer Liste nicht nur einfach hintereinanderweg schreiben, sondern zunächst paarweise in einer kleinen Liste gruppieren und dann diese kleinen Listen in einer großen Liste zusammenfassen. Etwa so:


  pullover = [
    ["Mo", "gelb"], 
    ["Di", "blau"],
    ["Mi", "pink"],
    ["Do", "rot"],
    ["Fr", "grün"],
  ]
  for paar in pullover do
    puts "Am " + paar[0] + " ist die Farbe " + paar[1]
  end

Beachte bitte, dass es sich hier bei der Variable pullover nicht um einen Hash handelt, sondern eine Liste! Ich habe die Schreibweise zwar so ähnlich wie bei einem Hash angeordnet, aber du erkennst an den eckigen Klammern, dass es nur Listen sind!

Welche Farbe hatte ihr Pullover am Mittwoch? Wir suchen das Wertepaar, das an der erste Stelle (dem Index 0 in der kleinen Liste) den Werte “Mi” hat und geben für die gefundene kleine Liste den Wert beim Index 1 aus.


  pullover = [
    ["Mo", "gelb"], 
    ["Di", "blau"],
    ["Mi", "pink"],
    ["Do", "rot"],
    ["Fr", "grün"],
  ]

  such_tag = "Mi" 
  for paar in pullover do
    if paar[0] == such_tag
      puts "Am " + such_tag + " ist die Farbe " + paar[1] + "!" 
      break
    end
  end

Pulloverfarbe mit Hash

Endlich. Wir nehmen einen Hash. Es scheint sich irgendwie aufzudrängen. Das mit dem Index scheint doch sehr kompliziert und unübersichtlich.


  pullover_hash = {
    "Mo" => "gelb",
    "Di" => "blau", 
    "Mi" => "pink", 
    "Do" => "rot",
    "Fr" => "grün",
  }

  for tag in pullover_hash.keys do 
    puts "Am " + tag + " ist die Farbe " + pullover_hash[tag]
  end

Die Frage Welche Farbe hatte ihr Pullover am Mittwoch? ist nun in nur einer Zeile beantwortbar.


  pullover_hash = {
    "Mo" => "gelb",
    "Di" => "blau", 
    "Mi" => "pink", 
    "Do" => "rot",
    "Fr" => "grün",
  }

  such_tag = "Mi" 
  puts "Am " + such_tag + " ist die Farbe " + pullover_hash[such_tag]

Warum nicht gleich so? Wie gesagt, die Lage der Dinge ist nicht immer so einfach. Was ist, wenn du dich für einen Hash entschieden hast, später aber neben der Farbe auch noch die Schuhe mit in dein Programm aufnehmen möchtest (du kennst das von Mama: Frauen haben gewöhnlich mindestens 5 Paar Schuhe)? Dann bist du mit einem Hash ziemlich aufgeschmissen. Überlege dir, wie es vielleicht doch mit einem Hash gehen könnte! Kleiner Tipp: der Wert, der zu einem Schlüssel in einem Hash abgelegt ist, kann auch eine Liste sein.

Üben mit Peter und Livia

Peter und Livia haben heute keine Lust zum Üben. Sie freuen sich schon auf das Spiel, das sie in der nächsten Lektion mit dir zu programmieren beginnen werden und sind vor lauter Aufregung zu keiner Übung mehr fähig.

Über Listen - Theorie Lektion 5

Erstellt von Frithjof Thu, 02 Aug 2007 20:01:00 GMT

Diese Lektion und vielleicht noch eine weitere, dann fängst du aber wirklich an, ein Spiel in Ruby zu programmieren. Jetzt lernst du ein sehr wichtiges Dings. Das Dings gehört zu den sogenannten Datenstrukturen.

Du weißt bereits, wie man eine Zahl oder eine Zeichenkette in Ruby verwendet. Was aber, wenn du gleich mehrere Zahlen oder Zeichenketten als ein Ganzes verwenden möchtest, sozusagen eine Liste mit Zahlen oder Zeichenketten?

Einfache Liste in Ruby

Schau dir diesen Code an:


# theorie_05.rb

zahlen_liste = [2, 4, 6, 8, 10]

planeten_liste = [
  "Merkur",  "Venus", 
  "Erde",    "Mars", 
  "Jupiter", "Saturn", 
  "Uranus", "Neptun",
]

irgendwas_liste = ["Mars", 95, "95", 95, "Quark"]

leere_liste = []

Du siehst, eine Liste ist ganz einfach zu erstellen. Der Anfang einer Liste wird mit der öffnenden eckigen Klammer [ markiert, das Ende der Liste mit der schließenden eckigen Klammer ]. Zwischen die Klammern schreibst die Glieder der Liste und trennst sie jeweils mit einem Komma ab. Ein Komma darf auch nach dem letzten Glied in einer Liste stehen, aber nicht vor dem ersten. Man bezeichnet die Dinge in einer Liste als Mitglieder oder Elemente der Liste. Eine Liste ohne Elemente heißt die leere Liste.

Eine Liste kann aus einheitlichen Elementen bestehen, also bspw. nur Zahlen. Sie kann aber auch Elemente verschiedenen Typs enthalten, also bspw. Zeichenketten und Zahlen. Ein Element einer Liste kann sogar selbst wieder eine Liste sein!

Eine Liste kann mehrere identische Elemente enthalten. Bspw. in der irgendwas_liste ist die 95 zweimal enthalten. Oder dreimal? Nein, für Ruby ist das Element 95 eine Zahl, das Element "95" aber eine Zeichenkette. Daher ist die Zahl 95 nur zweimal enthalten.

Verteile die Elemente einer Liste über mehrere Zeilen, damit es besser lesbar ist, was in der Liste so alles enthalten ist. Du brauchst die Liste also nicht in eine einzige Zeile zu quetschen.

Listen nennt man auch Array (engl. für Anordung, Reihe) oder Vektor. Genaugenommen ist eine Liste etwas mehr als ein Array. Bei einer Liste kennt jedes Element ihre unmittelbaren Nachbarelemente, entweder das davor und das danach oder nur eines von beiden. Das was wir hier in diesem Artikel als Liste kennen lernen ist daher genaugenommen nur ein Array, weil die Elemente sich selbst untereinander nicht kennen. Wir bleiben aber hier trotzdem bei dem Begriff Liste, weil man eher begreift, was gemeint ist.

Ausgabe einer einfachen Liste

Angenommen wir definieren eine Einkaufsliste mit den Elementen Butter, Milch, Honig und Brot. Dann können wir die Liste natürlich einfach mit dem puts Befehl ausgeben. Dabei wird jedes Element der Liste in einer neuen Zeile ausgegeben.

Ruby bietet dir aber noch eine schönere Ausgabe der Liste an. Auf englisch heißt das pretty print (schöner Druck). Verwende statt puts den Befehl pp (dabei steht pp für pretty print). Bevor du diesen Befehl in deinem Programm verwenden kannst, muss du ihn im Programm bekannt machen über ein require ‘pp’ am Anfang der Datei:


require 'pp'

# Einzelne Einkaufslisten
tegut = ["Butter", "Milch", "Honig", "Brot"]
obi   = ["Schrauben", "Leimholz", "Kabel", "Lampe"]
dm    = ["Duschbad für Mama",    "Deo für Papa", 
         "Haarspange für Livia", "Zahnbürste für Peter"]

# Gesamte Einkaufsliste, enthält die einzelnen Listen
einkauf = [tegut, obi, dm]

# Normale Ausgabe
puts einkauf

# Schickere Ausgabe mit pretty print
pp einkauf

# Ausgabe des dritten Elementes der Liste
pp einkauf[2]

Du siehst hier auch, dass eine Liste als Elemente wiederum Listen enthalten kann. Die Liste obi mit ihren Elementen "Schrauben", "Leimholz", "Kabel", "Lampe" wird in der Liste einkauf als zweites Element gespeichert.

Die gesamte Liste auszugeben ist gut und schön. Häufiger jedoch möchtest du nur bestimmte Elemente der Liste ausgeben. Ruby nummeriert die Elemente durch, sodass du einfach nur die Nummer des Elementes angeben musst. Das machst du, indem du direkt an den Variablennamen der Liste die gewünschte Nummer in eckige Klammern anhängst.

Aber Achtung: Ruby beginnt beim Durchnummerieren der Elemente in einer Liste immer mit 0, nicht mit 1! Möchtest du das dritte Element in der Einkaufsliste einkauf ausgeben, musst du somit einkauf[2] schreiben, nicht einkauf[3], weil das erste Element in der Liste nämlich nicht einkauf[1], sondern einkauf[0] ist.

Hash – Liste mit Index

Ruby kennt noch eine weitere wichtige listenähnliche Datenstruktur. Es ist eigentlich eine Art Doppelliste bestehend aus zwei Listen: eine Liste mit Namen und eine Liste, die für jeden Namen den zugehörigen Wert enthält. Den Namen nennt man auch Schlüssel oder englisch Key oder Index. Ein bestimmter Key darf in dieser Liste nur höchstens einmal vorkommen. Daher heißt diese Datenstruktur auch Indextabelle oder englisch Hashtable oder einfach nur ein Hash. Verwende einen Hash immer dann, wenn du schnell über einen Schlüsselnamen auf einen zugehörigen Wert zugreifen möchtest.

Der folgende Hash legt für jeden Namen eines deutschen Zahlwortes oder einer natürlichen Zahl von 1 bis 10 als zugehörigen Wert das Zahlwort in spanischer Sprache ab.


require 'pp'

deutsch_spanisch = {
  1        => "uno",
  "eins"   => "uno",
  2        => "dos",
  "zwei"   => "dos",
  3        => "tres",
  "drei"   => "tres",
  4        => "cuatro",
  "vier"   => "cuatro",
  5        => "cinco", 
  "fuenf"  => "cinco", 
  6        => "seis",
  "sechs"  => "seis",
  7        => "siete",
  "sieben" => "siete",
  8        => "ocho",
  "acht"   => "ocho",
  9        => "nueve",
  "neun"   => "nueve",
  10       => "diez",
  "zehn"   => "diez",
}
pp deutsch_spanisch
puts "Vier heisst auf Spanisch #{deutsch_spanisch['vier']}." 
puts "Acht heisst auf Spanisch #{deutsch_spanisch[8]}." 

Ein Hash wird ähnlich einer Liste definiert aber mit folgenden Unterschieden. Der Anfang und das Ende des Hash werden mit geschweiften Klammern (anstatt eckiger) markiert. Die Elemente des Hash werden zwar auch mit Komma getrennt, aber ein Element besteht immer aus einem Name-Wert-Paar. Zuerst schreibst du den Namen hin (eine Zahl oder eine Zeichenkette, oder …), dann einen nach rechts gerichteten Pfeil => und auf die rechte Seite des Pfeils schreibst du den Wert. Lies den Pfeil als wenn du sprichst: dem Namen (oder Key) sowieso ordne ich den Wert soundso zu.

Du siehst in unserem deutsch_spanisch Hash auch, dass es zwar gleiche Werte auf der rechten Seite geben kann, auf der linken Seite darf jeder Key aber nur höchstens einmal vorkommen. Kommt ein Key aber trotzdem auf der linken Seite mehrmals vor, dann speichert Ruby nur den zuletzt genannten Wert im Hash ab.


require 'pp'

deutsch_spanisch = {
  "acht" => "ocho",
  "acht" => "otscho",
}
puts "Acht heisst auf Spanisch #{deutsch_spanisch['acht']}." 

Dieser Hash hat nur ein Element! Der Wert für den Key acht wird beim zweiten mal überschrieben und hat am Ende nur einen Wert otscho.

Durchgehen von Listen und Hashes

Nicht selten muss man eine Liste durchgehen und mit jedem Element der Liste etwas bestimmtes unternehmen. Oder man möchte für alle Keys in einem Hash die Werte untersuchen. Schauen wir uns zunächst an, wie man für eine Liste alle Elemente durchgeht:



# Schleife über eine Liste

planeten = [
  "Merkur",  "Venus", 
  "Erde",    "Mars", 
  "Jupiter", "Saturn", 
  "Uranus", "Neptun",
]

for planet in planeten
  # Nur die Planeten ausgeben, in deren Namen ein e vorkommt.
  if planet.include?("e")
    puts planet
  end
end

Das Durchgehen einer Liste nennt man auch einen Loop über eine Liste machen. Loop (englisch für Schleife) deswegen, weil man schleifenartig ein Element nach dem anderen abklappert. Die Schleife beginnt mit dem for planet in planeten und endet mit end. Am Beginn der Schleife wird der Variablen planet bei jedem Durchgang der Wert des jeweils nächsten Elements zugewiesen. Diese Variable ist nur innerhalb der Schleife bekannt. Man nennt sie daher auch Schleifenvariable. Wenn alle Elemente in der Schleife behandelt wurden, setzt Ruby das Programm nach dem end fort.

Für einen Hash könnte ein Schleifendurchgang etwa so aussehen.



# Schleife über einen Hash

de_esp = {
  "eins"   => "uno",
  "zwei"   => "dos",
  "drei"   => "tres",
  "vier"   => "cuatro",
  "fuenf"  => "cinco", 
  "sechs"  => "seis",
  "sieben" => "siete",
  "acht"   => "ocho",
  "neun"   => "nueve",
  "zehn"   => "diez",
}

for key in de_esp.keys
  if de_esp[key].include?("t")
    puts de_esp[key]
  end
end

Die Liste aller Keys für den Hash de_esp bekommen wir mit de_esp.keys. Diese Liste gehen wir durch und verwenden den jeweiligen Key dazu, uns den zugehörigen Wert aus dem Hash zu holen—hier das Zahlwort auf Spanisch. Enthält der Hashwert (das spanische Zahlwort) ein t im Namen, dann geben wir es mit puts aus.

Natürlich hat Ruby noch mehrer Möglichkeiten, Schleifen über Listen und Hashes zu machen. Aber die hier vorgestellte for-Schleife ist gut lesbar und fürs erste ausreichend.

Üben mit Peter und Livia

Livia: Schreibe die Einkaufsliste von oben als Hash um. Verwende dabei die Variablennamen der einzelnen Einkaufslisten (tegut, obi, dm) als Keys im Hash! Was fällt dir bei der pretty print Ausgabe bezüglich der Reihenfolgen von Liste und Hash auf?

Peter: Zähle bitte auf, wozu man alles die eckigen Klammern [ ] bei Listen und Hashes benutzt!

Hinein und wieder heraus - Theorie Lektion 4

Erstellt von Frithjof Sun, 22 Jul 2007 04:44:00 GMT

Die letzte Lektion lehrte dich, was in Ruby eine Methode ist—eine Menge von Codezeilen, denen du einen Namen verpasst. Du rufst die Methode auf, indem du den Namen der Methode verwendest.

Eine Methode ist aber eigentlich noch mehr. Sie ist wie eine Maschine. Das Aufrufen der Methode bei ihrem Namen ist, als wenn du eine Maschine einschaltest. Wenn die Maschine fertig ist, hört sie auf zu arbeiten—die Methode hat alle Codezeilen ausgeführt.

Und wo ist das mehr? Bevor du eine Maschine arbeiten lässt, kannst du normalerweise etwas in die Maschine hineingeben. Ist sie fertig mit ihrer Arbeit, dann liefert sie ein fertiges Produkt. Schau dir die Goldmaschine in der Abbildung an. Sie erhält als Eingabe Goldsteine (Felsbrocken, in denen Gold eingeschlossen ist) und liefert als Ausgabe Goldbarren. Wie sie das genau macht, wissen wir nicht. Irgendwie wird sie die Steine erhitzen, das Gold schmilzt und läuft heraus, wird aufgefangen in Formen gegossen… Egal, Hauptsache sie macht, wozu sie konstruiert wurde.

Oder stell dir eine Kaffeemaschine vor! Es ist Sonntagmorgen und du möchtest Mama, die noch friedlich schläft, mit einem Tässchen Kaffee überraschen. Du gibst folgendes in die Kaffeemaschine hinein:
  1. Ein wenig Wasser
  2. Einen Kaffeefilter
  3. Etwas Kaffeepulver

Du schaltest die Maschine ein. Sofern sie korrekt funktioniert, wird sie dir Kaffee zubereiten, der in eine Glaskanne herausläuft. Nun nur noch in eine schöne Sonntagstasse füllen, etwas Sahne dazu, vielleicht Zucker, Untertasse nicht vergessen (Mamas mögen sowas!), auf das Tablett neben das leckere Marmeladebrötchen und das frisch gekochte Ei (das hast du mit einer anderen Maschine erledigt) und ab zu Mama! So kriegst du sie immmer wach, egal wie früh es ist…

Eine Methode ist wie eine Maschine, hatte ich gesagt. Wie gibst du nun etwas in die Methode hinein? Und wie bekommst du etwas aus der Methode heraus? Schauen wir uns an, wie das mit einer Rubymethode aussieht:


# theorie_04.rb

def kaffee_maschine(wasser, filter, pulver)
  # Die Maschine beginnt mit dem Heizen des Wassers
  puts "=> #{wasser} Wasser werden heisser und heisser..." 
  puts "=> Pumpe das Wasser durch ein Roehrensystem..." 
  puts "=> Und lasse es auf #{pulver} Pulver in den #{filter} fallen.\n" 
  # Das heisse Wasser laeuft durch das Pulver im Filter in die Glaskanne
  # darunter, das geht von alleine, da braucht die Maschine nichts 
  # weiter zu machen 
  # ...
  # Maschine ist fertig:
  puts "=> Fauchen und Dampf ablassen als Zeichen, dass ich fertig bin..." 

  # Fertigen Kaffee zurueckliefern
  return "Bitte schön: Glaskanne mit #{wasser} leckerem Kaffee" 
end

wasser = "2 Tassen (okay Papa kriegt auch eine)" 
filter = "Kaffeefilter Groesse 4 (der aus dem Aldi)" 
pulver = "Mhhmm, riecht das lecker. 2 Löffel!" 

kaffee_fuer_mama = kaffee_maschine(wasser, filter, pulver)

puts "Kaffee ist fertig!" 
puts kaffee_fuer_mama

Schauen wir uns den Code genauer an!

Zunächst definierst du eine Methode mit dem Namen kaffee_maschine. Die Methode umfasst alle Codezeilen bis zum zugehörigen end. Die Methode selbst kocht uns natürlich keinen echten Kaffee, es ist ja nur ein Rubyprogramm. Daher gibt es nur aus, was es tun würde, wenn es eine echte Kaffeemaschine wäre.

Hier ist aber etwas neu. Hinter dem Namen der Methode steht ein Paar runde Klammern, und innerhalb der Klammern stehen drei Variablennamen wasser, filter und pulver. Merke also, wenn du möchtest, dass jemand etwas in deine Methode (Maschine) hineintun können soll, dann schreibst du hinter den Methodennamen in runde Klammern für jedes Ding einen Variablennamen.

Innerhalb der Methode kannst du dann die Variablen wie ganz gewöhnliche Variablen verwenden und irgendetwas damit machen.

Ist die Methode (Maschine) fertig, kann sie das Ergebnis (das fertige Produkt) zurückgeben. Das passiert in der letzten Zeile vor dem Ende der Methode mit dem return Befehlt. Nach dem return steht das, was du deine Methode zurückgeben lassen möchtest. Im Beispiel oben ist es einfach die Mitteilung, dass in der Glaskanne 2 Tassen Kaffee sind.

Noch einige Besonderheiten

Die Variablen, die du hinter dem Methodennamen aufzählst, sind wirklich nur innerhalb der Methode verwendbar. Außerhalb der Methode sind die Variablen nicht bekannt. Wir haben zwar unterhalb der Methode diese 3 Variablen noch einmal stehen, das sind aber komplett andere Variablen, die heißen bloß genauso! Wir könnten sie oder die Variablen hinter dem Methodennamen ganz anders nennen, und das Programm funktionierte noch immer. Bedenke nur, dass du die Variablen dann auch dort ändern musst, wo du sie verwendest.



def kaffee_maschine(wasser, filter, pulver)
  # Die Maschine beginnt mit dem Heizen des Wassers
  ...
  return "Bitte schön: Glaskanne mit #{wasser} leckerem Kaffee" 
end

wieviel_wasser = "2 Tassen (okay Papa kriegt auch eine)" 
welcher_filter = "Kaffeefilter Groesse 4 (der aus dem Aldi)" 
wieviel_pulver = "Mhhmm, riecht das lecker. 2 Löffel!" 

kaffee_fuer_mama = kaffee_maschine(wieviel_wasser, welcher_filter, wieviel_pulver)

puts "Kaffee ist fertig!" 
puts kaffee_fuer_mama

Bei der Rückgabe des Ergebnisses kannst du das Wörtchen return auch weglassen, wenn die Rückgabe in der letzten Zeile der Methode erfolgt. Das muss nicht zwingend immer die letzte Zeile vor dem end sein. Sondern es muss die letzte Zeile sein, die Ruby während der Abarbeitung deiner Methode ausführt, bevor Ruby die Methode verlässt. Hast du beispielsweise eine IF-Abfrage in der Methode, könnte es mit und ohne return so aussehen.


# Methode mit return

def mehr_kaffee(soviel_hatte_sie_schon)
  if soviel_hatte_sie_schon > 2
    return "gibt nix mehr!" 
  else
    return "Okay, noch eine Tasse!" 
  end
end


# Methode ohne return

def mehr_kaffee(soviel_hatte_sie_schon)
  if soviel_hatte_sie_schon > 2
    "gibt nix mehr!" 
  else
    "Okay, noch eine Tasse!" 
  end
end

Die Zeile, die im letzten Codebeispiel als letztes ausgeführt wird, hängt ab vom Wert, der in der Variablen soviel_hatte_sie_schon übergeben wurde. Ist der Wert größer 2, gibt die Methode “gibt nix mehr!” zurück, auch wenn diese Codezeile nicht die letzte in der Methode ist. Sie ist aber die letzte, die unter diesen Bedingungen von Ruby ausgeführt wird. Denn in den else-Zweig gelangt Ruby nur, wenn der Wert der übergebenen Variablen kleiner oder gleich 2 ist.

Üben mit Peter und Livia

Livia: Schreibe eine Methode mit dem Namen maximum, die zwei Zahlen entgegen nimmt, dann entscheidet, welche Zahl die größere ist und diese dann als Ergebnis zurückliefert.

Peter: Schreibe eine Methode mit dem Namen verketten, die zwei Zeichenketten entgegen nimmt, sie beide aneinander hängt und die so neu entstandene Zeichenkette zurück gibt.

Programme aufteilen - Theorie Lektion 3

Erstellt von Frithjof Sat, 14 Jul 2007 21:28:00 GMT

Du bist noch immer unterwegs zu Lektion 11, wo du dann beginnen wirst, ein Spiel zu programmieren.

Aus Lektion 7 weißt du bereits, dass lange Programme zunehmend schwieriger zu lesen sind. Du reihst eine Zeile an die nächste. Was machst du, wenn du im Verlauf des Programms an eine Stelle kommst, wo du genau denselben Code brauchen würdest, den du weiter oben schon einmal verwendet hast? Nochmal hinschreiben? Naja, das stinkt ja bereits schon beim nur dran denken. Du erinnerst dich, was stinkender Code ist?

Du lernst in dieser Theorielektion eine Möglichkeit kennen, die du sehr häufig brauchen wirst wenn es darum geht, gleichen Code wiederzuverwenden, ohne ihn erneut hinschreiben zu müssen.

Die Idee ist ganz einfach. Du schreibst deine Zeilen Code, die du häufiger verwenden möchtest, in eine Datei und gibst ihnen dabei einen Namen. Ja richtig gehört, du denkst dir einen Namen für die Programmzeilen aus. Immer dann wenn du anschließend die Zeilen Code verwenden möchtest, verwendest du stattdessen nur den Namen. Du rufst den Code bei seinem Namen!

Sagen wir, du willst in deinem Programm an verschiedenen Stellen die Aufforderung zur Eingabe einer Zahl verwenden


puts "Bitte gib eine Zahl größer 0 ein:" 

Du denkst dir als Namen frag_sie aus. Das Benennen des Codes sieht dann so aus:


def frag_sie
  puts "Bitte gib eine Zahl größer 0 ein:" 
end

Das Schlüsselwort def leitet die Benennung des Codes ein, gefolgt von dem Namen den du dir überlegt hast. Am Ende der Codezeilen wird die Benennung mit end abgeschlossen.

Nun kannst du den benannten Codeabschnitt verwenden, indem du ihn bei seinem Namen aufrufst. Dabei musst du nur darauf achten, dass der Aufruf natürlich nach der Benennung steht.


# theorie_03.rb

# Zuerst den Code mit einem Namen versehen
def frag_sie
  puts "Bitte gib eine Zahl größer 0 ein:" 
end

# Dann den benannten Code verwenden
frag_sie
a = gets

# Noch eine Zahl verlangen
frag_sie
b = gets

# und noch eine, weils so einfach ist
frag_sie
c = gets

Das Schlüsselwort def leitet sich von Definieren ab. Das Gebilde vom def über den Namen, den Code bis zum end hin nennt man auch ein Unterprogramm, weil es innerhalb eines anderen Programmes existiert. Andere Bezeichnungen dafür sind noch Subroutine, Funktion oder Methode.

Lass uns von nun an den Begriff Methode (d.h. eine Handlungsanweisung oder Art und Weise) dafür verwenden. Denn der benannte Code stellt eine Folge von Anweisungen dar, die bei Aufruf so ausgeführt werden.

Du hast also soeben deine erste Methode definiert, die Methode heißt frag_sie.

-
Update 15.07.2007

Üben mit Peter und Livia

Gestern Abend war es schon spät, Peter und Livia bereits in tiefen Schlaf gefallen. Heute morgen waren sie dann ganz enttäuscht, dass sie keine Übung zum Theorieartikel beisteuern konnten. Daher hier noch eine kleine Übung zu Methoden. Wie üblich erst selbst versuchen, wenn du nicht weiterkommst, Mama fragen, als letztes kannst du dann in der Lösung nachsehen.

Peter und Livia: Schreibe die Methode frag_sie so um, dass sie nach dem Namen der Anwenderin fragt. Schreibe zusätzlich noch eine Methode mit Namen sie_sagte, die ausgibt, was die Anwenderin eingegeben hatte. Verwende danach beide Methoden.

Lösungen

def frag_sie
  puts "Wie heisst du? " 
end

def sie_sagte
  a = gets
  puts "Aha, du heisst also #{a}" 
end

frag_sie
sie_sagte

Lass uns reden! 1

Erstellt von Frithjof Fri, 29 Jun 2007 22:19:00 GMT

Na schön, das mit dem Ausgeben klappt ja nun schon recht gut. Irgendwie kann es aber nicht alles sein, wenn das Rubyprogramm nur gestartet wird, ein wenig was arbeitet und dann irgendetwas ausgibt. Das ist, als wenn dir jemand gegenüber steht, der nur plaudert und dich nicht zu Wort kommen lässt. Dabei hast du auch etwas zu sagen, oder?

Ganz so schlimm ist es aber doch nicht. Ein wenig hattest du schon zu sagen – das Fahrrad konntest du mit der L- und R-Taste steuern und mit der X-Taste vom Rad absteigen.

In diesem Artikel zeige ich dir, wie du dein Rubyprogramm dazu bekommst, sich mit dem, der es ausführt zu unterhalten.

Eingaben von Anwenderinnen abfragen

Schauen wir uns gleich folgendes Programm an:


# theorie_02.rb

print "Gib eine Zahl a zwischen 1 und 100 ein! a=" 
a = gets
a = a.chomp
a = a.to_i

zahl_teilbar_durch_3 = false
if a % 3 == 0 
  zahl_teilbar_durch_3 = true
end

puts "Du hast a=#{a} eingegeben." 
if zahl_teilbar_durch_3
  puts "#{a} ist durch 3 teilbar." 
else
  puts "#{a} ist nicht durch 3 teilbar." 
end

Wir fordern die Anwenderin zunächst mit der Ausgabe Gib eine Zahl a zwischen 1 und 100 ein! dazu auf, na was wohl, natürlich eine Zahl einzugeben, die größer oder gleich 1 aber kleiner oder gleich 100 ist. Damit sie weiß, wo beim Tippen die Zahl erwartet wird, schreiben wir noch das a= hin. Direkt nach dem = wird dann an der Kommandozeile das erscheinen, was sie eintippt. Ruby wartet nun darauf, solange, bis sie die Enter-Taste drückt.

Dann geht unser Programm zur nächsten Zeile und liest das was eingegeben wurde von der Kommandozeile in die Variable a ein. In a steht nun eine Zeichenkette mit dem Sonderzeichen für die Entertaste \n am Schluß.

Dieses Sonderzeichen schneiden wir mit dem Befehl chomp (englisch: mampfen) ab. Den Befehl oder besser die Nachricht chomp schicken wir direkt an die Variable a. Das kennst du auch schon aus den früheren Lektionen. Wir trennen dabei die Nachricht chomp vom Variablennamen mit dem Punkt . ab. Das was nach dem Abschneiden von \n noch übrig bleibt, weisen wir wieder der Variablen a zu.

In der nächsten Zeile schicken wir wieder eine Nachricht an a und zwar lautet diese to_i. Das ist die Abkürzung für englisch to integer, d.h. mache eine ganze Zahl daraus!. Den dadurch entstehenden Zahlenwert weisen wir wieder der Variablen a zu. Wir könnten auch jedesmal eine andere Variable verwenden. Da wir aber eigentlich nur am Zahlenwert interessiert sind, können wir den Wert von a ohne Sorge überschreiben.

So, jetzt steht in a keine Zeichenkette (in Ruby String genannt) mehr, sondern eine Zahl (in Ruby Fixnum oder Integer genannt).

Mit der Zahl a führen wir nun den Test durch, ob sie durch 3 teilbar ist. Dazu berechnen wir den Rest bei der Division durch 3. Den Rest erhalten wir mit dem Operator %. Nur wenn der Rest beim Teilen durch 3 Null ist, ist die Zahl (restlos) durch 3 teilbar. Ob sie das ist, merken wir uns in der Variablen zahl_teilbar_durch_3.

Okay, nun können wir der Anwenderin Bescheid geben, was wir herausgefunden haben. Wir zeigen ihr zuerst nochmal das, was sie eingegeben hat. Halt, das stimmt eigentlich nicht. Sie hat ja genau genommen noch die Entertaste eingegeben. Aber das will sie sicher nicht wissen.

Schließlich verraten wir ihr noch, ob die Zahl durch 3 teilbar ist oder nicht.

Und wenn sie nicht auf mich hört?

Versuche das Programm zu starten und gib mal eine Zahl ein, die größer als 100 ist. Es geht! Warum auch nicht. Wir prüfen nirgendwo, ob die eingegebene Zahl auch tatsächlich einen Wert hat, den wir gefordert haben. Das überlasse ich dir als Übung.

Die Büchse der Pandora

Ein Programm, das man startet, es dann irgendetwas tut und sich am Ende mit irgendeiner Ausgabe verabschiedet ist einfach. Wobei einfach hier nicht im Sinne zu verstehen ist, dass solche Programme nicht auch kompliziert oder auch schwierig zu entwickeln sein könnten. Nein, mit einfach meine ich, dass der Ablauf des Programmes recht übersichtlich ist. Man gibt etwas ein, wartet und schaut sich dann das Ergebnis an. Fertig.

Sobald du aber ein Programm während seiner Laufzeit (englisch: runtime), also nachdem du es gestartet hast und es noch nicht fertig ist, für die Anwenderinnen öffnest, verliert es seine einfache Ablaufstruktur.

Kommunizierst du mit einem realen Menschen direkt von Angesicht zu Angesicht, kannst du auf ihr Reden sofort reagieren. Du siehst ihre Mimik und Gestik und hörst auch auf den Ton, wie sie etwas sagt. Daraus kannst du neben den Worten, die von ihr an dein Ohr dringen, sogar noch eine zusätzliche Bedeutung entnehmen.

Die Kommunikation mit einem realen Menschen, allerdings nicht direkt, sondern nun über den Umweg eines Computerprogramms ist da anders. Der Austausch von Information geschieht hier zeitlich versetzt. Du musst dir während du das Programm entwickelst, Gedanken darüber machen, wie du die Fragen stellst, welche Antworten du erwartest und wie du wiederum auf diese Antworten reagieren möchtest.

Angenommen, du schreibst ein Programm, dass zwei Zahlen a und b einliest und als Ausgabe den Quotienten aus a/b ausgeben soll. Du forderst die spätere Anwenderin deines Programms etwa so auf:


"Gib die erste Zahl größer als 1 ein! a=" 
...
"Gib die zweite Zahl größer als 1 ein! b=" 

Nun hast du in deinem Programm zwei Variablen a und b. Du weißt aber während du das Programm entwickelst nicht, welchen Wert genau a und b haben werden. Du wirst sicher nicht dabei sein, wenn sie irgendwo auf der Welt vor ihrem Computer sitzt und dein Programm ausführt, du siehst nicht, was sie eingibt. Es wäre leichtsinnig sich darauf zu verlassen, dass sie wirklich das macht, wozu du sie aufgefordert hast, nämlich zwei Zahlen einzugeben, die beide je größer als 1 sind. Was, wenn sie für b eine Null eintippt? Die Division durch Null ist in der Mathematik nicht erlaubt. Ruby wird sich mit einer Fehlermeldung beklagen. Je mehr Fehlermeldungen dein Programm produziert, ums so mehr wird das Vertrauen in deine Software sinken. Sie wird nie sagen Oh, ich habe eine Null eingegeben, das war mein Fehler. Stattdessen wird sie sagen So ein dummes Programm, es lässt einfach zu, dass ich eine Null eingeben kann. Dabei weiß doch jeder, dass man durch Null nicht dividieren kann. Und sie wird sich ein anderes Divisionsprogramm suchen.

Sobald dein Programm interaktiv genutzt werden soll—also nicht nur Ausgaben produziert, sondern auch Eingaben entgegen nimmt—bist du gezwungen, jede Eingabe zu prüfen. Liegt sie nicht in dem Bereich, mit dem du weiterarbeiten kannst, dann gibt dein Programm eine freundliche Hinweismeldung zurück.

Es gibt verschiedene Formen von fehlerhaften Eingaben:

  1. Unabsichtliche Fehleingaben Vielleicht waren deine Anweisungen nicht verständlich genug. Oder sie hat sie nicht richtig gelesen, oder hat einfach die falsche Taste erwischt.
  2. Bewusste Fehleingaben Sie legt es darauf an, dein Programm in einen Fehlerzustand zu versetzen. Entweder, um dir zu zeigen, dass du dein Programm nachlässig entwickelt hast, oder sogar, um dir absichtlich zu schaden. Insbesondere ist Software, die über das Internet genutzt wird böswilligen Angriffen ausgesetzt. Sie will über dein Programm hinaus eigentlich an die dahinterliegende Hardware herankommen, bpsw. an den Server und seine Festplatten, um dort an weitere Daten zu gelangen, die du in deinem Programm niemals freiwillig über das Internet zu ihr geschickt hättest.

Du wirst bald merken, dass du manchmal viel mehr Code dafür schreiben musst, um diese Fehleingaben sicher abfangen zu können, als für das, was du eigentlich mit deinem Programm anbieten möchtest.

Üben mit Peter und Livia

  1. Livia: Ich will aber keine Zahl zwischen 1 und 100 eingeben. Vielleicht will ich überhaupt keine Zahl eingeben. Ändere obiges Programm so ab, dass ich dann eine höfliche Fehlermeldung von dir bekomme!
  2. Peter: Entwickle ein Programm, das folgendes macht:
    • Es verlangt von der Anwenderin eine Zahl zwischen 1 und 5. Nennen wir sie n.
    • Es fragt dann die Anwenderin nacheinander n mal nach je einer weiteren Zahl im Bereich von 1 bis 1000.
    • Am Schluß teilt es der Anwenderin mit, ob die eingegebenen n Zahlen jede für sich eine Primzahl ist. Eine Primzahl ist eine Zahl, die nur durch 1 und sich selbst teilbar ist.

schreib und schreibe oder puts und print 2

Erstellt von Frithjof Sun, 24 Jun 2007 20:30:00 GMT

Das ist der erste reine Theorieartikel. Ich schiebe damit den Start des neuen Blocks an Lektionen noch etwas auf. Wir wollen ab Lektion 11 ein Spiel programmieren. Darauf möchte ich dich mit den folgenden Theorielektionen etwas besser vorbereiten.

In Lektion 7 hatten wir schon ein recht umfangreiches Programm geschrieben. Wir konnten ein Fahrrad auf der Straße vor einer Häuserreihe mit der Tastatur hin und her fahren lassen. Wenn du dir das Programm zur Wiederholung nochmals anschaust, stellst du sicher fest, dass es sich trotz der paar Wochen Abstand immer noch gut lesen lässt.

Das liegt zu einem großen Teil daran, dass wir viele deutsche Befehle und Variablennamen verwenden. Wenn dort der Befehl schreib dach steht, ist klar, was passiert – es wird etwas ausgegeben, was das Dach eines Hauses darstellen soll.

Die deutschen Namen für die Variablen sind sehr okay. Die deutschen Befehle wie schreibe, schreib, lies_ein_zeichen und dergleichen wollen wir aber schrittweise durch die richtigen Rubybefehle ersetzen. Keine Angst, dadurch werden unsere Programme nicht unleserlicher. Im Gegenteil, oft sind die deutschen Begriffe etwas länger und machen den Programmcode etwas zu dicht.

Wie du dir sicher denken kannst, sind die echten Befehle in Ruby hauptsächlich in englischer Sprache, entweder als komplettes Wort oder als Abkürzung. Schauen wir uns also als erstes heute einmal die Befehle für die Ausgabe genauer an.

schreibe oder puts

Mit dem schreibe Befehl können wir Text (Zeichenketten) oder Zahlen ausgeben. Am Ende der Ausgabe wird dabei immer die Zeile abgeschlossen, sodass jede weitere Ausgabe in der Zeile darunter beginnt. Etwa so:


schreibe "Dies ist der Text der Ausgabe" 

Den Befehl schreibe habe ich in der Datei rubykids.rb festgelegt. Der echte Rubybefehl, der dasselbe macht wie schreibe lautet puts. puts steht für die englischen Wörter put string oder put as string oder output as string, was soviel heißt wie gib es als Zeichenkette aus. Du kannst somit überall, wo wir den Befehl schreibe verwendet haben, ihn durch puts ersetzen.

Nach dem Befehl selbst folgt der Text oder die Zahl die ausgegeben werden soll. Folgt stattdessen der Name einer Variablen, dann wird nicht der Name, sondern der Inhalt der Variablen ausgegeben. Mehrere Ausgabeargumente lassen sich durch je ein Komma trennen. puts beginnt dann allerdings hinter jedem Argument einen neue Zeile (schreibe macht das ebenfalls).


puts "Erste Zeile", "Zweite Zeile", 5*3

Die Ausgabe sieht dann so aus:


C:\entwicklung>ruby test.rb
Erste Zeile
Zweite Zeile
15

C:\entwicklung>

schreib oder print

Der Befehl schreib arbeitet fast genauso wie schreibe, allerdings springt er nach der Ausgabe nicht in eine neue Zeile. So wird jede weitere Ausgabe direkt danach angefügt.

Der echte Rubybefehl, der dasselbe macht lautet print (englisch drucken). Auch ihm kannst du mehrere Argumente durch Komma getrennt mitgeben.

Nehmen wir bspw. den obigen Code und ersetzen nur puts durch print dann siehst du den Unterschied:


print "Erste Zeile", "Zweite Zeile", 5*3

Die Ausgabe ist eine einzige Zeile, alle Argumente sind aneinander geklatscht:


C:\entwicklung>ruby test.rb
Erste ZeileZweite Zeile15
C:\entwicklung>

Du kannst aber mit print natürlich auch eine neue Zeile erzeugen. Weißt du noch, wie das komische Sonderzeichen für den Zeilenumbruch war? Wir hatten es in den letzten Lektionen mal verwendet – \n. Wenn wir die beiden ersten Argumente mit dem Newline Zeichen abschließen, dann wird jedes Argument in einer eigenen Zeile ausgegeben, obwohl wir den print Befehl verwenden:


print "Erste Zeile\n", "Zweite Zeile\n", 5*3

Ausgabe:


C:\entwicklung>ruby test.rb
Erste Zeile
Zweite Zeile
15
C:\entwicklung>

Üben mit Peter und Livia

In den Theorieartikeln werden dir Peter und Livia ein paar Aufgaben stellen, die du selbstständig zur weiteren Übung lösen kannst. Wenn du dir eine Lösung erarbeitet hast, kannst du sie gerne als Kommentar hier an den Artikel anfügen - würde mich freuen. Nutze die Möglichkeit zum Kommentar auch, wenn du Fragen hast, oder mit einer Aufgabe nicht weiterkommst.

  1. Livia: Mit dem Body Mass Index kannst du feststellen, ob du mehr Sport treiben solltest oder ob das mit dem Nachschlag beim Essen okay ist. Der Index berechnet sich aus deinem Körpergewicht (in kg) und deiner Körpergröße (in m) nach der Formel: Gewicht / (Größe*Größe). Schreibe ein Rubyprogramm, das für Gewicht und Größe je eine Variable definiert und dann den BMI ausgibt.
  2. Peter: Schreibe ein Rubyprogramm, dass in einer Schleife alle Zahlen von 1 bis 20 ausgibt. Versuche das Programm so zu schreiben, dass die Ausgabe stets in derselben Zeile erfolgt. Die Ausgabe von 2 überschreibt also die vorherige Ausgabe von 1, die Ausgabe von 3 die von 2 usw. Versuche das Programm so zu verändern, dass es nur aus einer einzigen Zeile Rubycode besteht (die Zeile für require ‘rubykids’ zählt natürlich nicht mit).
Lösungen

require 'rubykids'

# 1. Livia
gewicht = 80
groesse = 1.85
bmi = gewicht / (groesse*groesse)
print "Der BMI bei Gewicht ", gewicht, " und Groesse ", groesse, " ist ", bmi

# 2. Peter
1.biszu(20) { |zahl| print "\r", zahl; schlafe_kurz }