Eigene Methoden schreiben

Wir haben gesehen, dass Schleifen und Iteratoren uns die Möglichkeit bieten, dasselbe immer und immer wieder zu tun. Manchmal will zwar man dasselbe öfter machen, aber an unterschiedlichen Stellen im Programm. Stellen wir uns vor, wir würden einen Fragebogen für einen Psychologie-Studenten schreiben. Bei den Studenten, die ich kannte, und den Fragebögen, die sie mir gegeben haben, würde dies etwa so aussehen:

puts 'Hallo und danke, dass du dir die Zeit nimmst'
puts 'und mich bei diesem Experiment unterstützt.'
puts 'Diese Umfrage geht um deine Meinung über '
puts 'italienisches Essen. Denke einfach nur an '
puts 'italienisches Essen und versuche, ganz ehrlich'
puts 'zu antworten, entweder mit "ja" oder "nein".'
puts 'Meine Umfrage hat nicht mit Bettnässen zu tun.'
puts

# Wir stellen ein paar Fragen, ignorieren aber die Antworten.

richtig = false
while (not richtig)
  puts 'Magst du Spaghetti Bolognese?'
  antwort = gets.chomp.downcase
  if (antwort=='ja' or antwort=='nein')
    richtig=true
  else
    puts 'Bitte antworte mit "ja" oder "nein"'
  end
end

richtig = false
while (not richtig)
  puts 'Magst du Maccaroni mit Käse?'
  antwort = gets.chomp.downcase
  if (antwort=='ja' or antwort=='nein')
    richtig=true
  else
    puts 'Bitte antworte mit "ja" oder "nein"'
  end
end

# Diesmal merken wir uns die Antwort!
richtig = false
while (not richtig)
  puts 'Nässt du dein Bett?'
  antwort = gets.chomp.downcase
  if (antwort=='ja' or antwort=='nein')
    richtig=true
    if (antwort=='ja')
      bettnaesser=true
    else
      bettnaesser=false
    end
  else
    puts 'Bitte antworte mit "ja" oder "nein"'
  end
end

richtig = false
while (not richtig)
  puts 'Magst du Penne arrabiata?'
  antwort = gets.chomp.downcase
  if (antwort=='ja' or antwort=='nein')
    richtig=true
  else
    puts 'Bitte antworte mit "ja" oder "nein"'
  end
end

richtig = false
while (not richtig)
  puts 'Magst du Calzone?'
  antwort = gets.chomp.downcase
  if (antwort=='ja' or antwort=='nein')
    richtig=true
  else
    puts 'Bitte antworte mit "ja" oder "nein"'
  end
end

# und hier kommen noch weitere Fragen...

puts
puts 'Auswertung'
puts 'Danke, dass du dir Zeit für die Umfage genommen hast.'
puts 'Tatsächlich hat die Untersuchung aber nichts mit '
puts 'italienischem Essen zu tun. Es ging ums Bettnässen.'
puts 'Alle anderen Fragen sollten nur die Aufmerksamkeit '
puts 'ablenken, um ehrliche Antworten zu erhalten.'
puts 'Vielen Dank nochmals.'
puts
puts bettnaesser
Hallo und danke, dass du dir die Zeit nimmst
und mich bei diesem Experiment unterstützt.
Diese Umfrage geht um deine Meinung über
italienisches Essen. Denke einfach nur an
italienisches Essen und versuche, ganz ehrlich
zu antworten, entweder mit "ja" oder "nein".
Meine Umfrage hat nicht mit Bettnässen zu tun.

Magst du Spaghetti Bolognese?
ja
Magst du Maccaroni mit Käse?
nein
Nässt du dein Bett?
nein
Magst du Penne arrabiata?
ja
Magst du Calzone?
nein

Auflösung
Danke, dass du dir Zeit für die Umfage genommen hast.
Tatsächlich hat die Untersuchung aber nichts mit
italienischem Essen zu tun. Es ging ums Bettnässen.
Alle anderen Fragen sollten nur die Aufmerksamkeit
ablenken, um ehrliche Antworten zu erhalten.
Vielen Dank nochmals.

false

Dies war ein ziemlich langes Program, mit vielen Wiederholungen. (Alle diese Teile Code über das italienische Essen waren identisch und die Frage übers Bettnässen war nur ein bisschen anders.) Wiederholung ist eine schlechte Angewohnheit. Andererseits können wir dies nicht in eine große Schleife oder Iterator packen, da wir ja manchmal etwas zwischen den Fragen auch machen wollen. In solchen Situationen braucht man Methoden. Und die schreibt man so:

def bellen
  puts 'wau '
end

Wie?... unser Programm hat gar nicht gebellt. Und warum nicht? Weil wir ihm nicht gesagt haben, dass es bellen soll. Wir haben ihm gesagt, wie man bellt, jedoch nie, dass es bellen soll. Versuchen wir es noch einmal:

def bellen
  puts 'wau '
end
bellen
bellen
puts 'wuff wuff wuff'
bellen
bellen

wau
wau
wuff wuff wuff
wau
wau

Ah, viel besser. (Info am Rande: Wir haben es in der Mitte mit einem englischen Hund zu tun, der sagt nämlich "wuff".)

Wir haben also eine Methode bellen definiert. (Methodennamen beginnen mit einem kleinbuchstaben, genau wie Variablennamen. Es gibt ein paar Ausnahmen wie + und ==.) Aber müssen Methoden nicht immer zu Objekten gehören? Ja, ganeu, das stimmt. Und in diesem fall hier, wie auch bei puts und gets gehört die Methode zu dem Objekt,d as das ganze Programm darstellt. Im nächsten Kapitel werden wir kennenlernen, wie man Methoden mit anderen Objekten assoziiert. Aber erst kümmern wir uns um ...

Methoden-Parameter

Sicher hast du schon gemerkt, dass manche Methoden (wie gets, to_s, reverse...) auf Objekten einfach aufgerufen werden. A ndere Methoden (wie +, -, puts...) benötigen Parameter, damit das Objekt weiß, was es genau tun soll. Du würdest je auch nicht wissen, was zu tun ist, wenn du nur 5 + bekommst, oder? Damit wird der 5 gesagt, dass addiert werden soll, doch es wird nicht gesagt, was addiert wird.

Um unserer bellen-Methode einen Parameter zu übergeben (der angibt, wie oft gebellt werden soll), schreiben wir dies:

def bellen num
  puts 'wau '*num
end

bellen 5
bellen 3
puts 'wuff wuff wuff'
bellen 6
bellen 4
bellen  # hier sollte ein Fehler gemeldet werden,
        # da der Parameter fehlt
wau wau wau wau wau
wau wau wau
wuff wuff wuff
wau wau wau wau wau wau
wau wau wau wau
bellen2.rb:10:in `bellen': wrong number of arguments (0 for 1) (ArgumentError)
  from bellen2.rb:10

num ist in der Methode bellen eine Variable, die auf den Wert zeigt, der als Parameter übergeben wird. Ich sag's noch einmal, weil es recht verwirrend ist: num ist in der Methode bellen eine Variable, die auf den Wert zeigt, der als Parameter übergeben wird. Wenn ich also bellen 3 aufrufe, dann ist der Parameter 3 und die Variable num zeigt drauf.

Wie du siehst, ist der Parameter jetzt erforderlich. Womit soll schließlich 'wau' multipliziert werden, wenn du ihm diese Information nicht sagst? deine armer Computer weiß es einfach nicht.

Wenn man Objekte in Ruby vergleicht mit Nomen im Deutschen, so sind die Methoden wie die Verben und man kann die Parameter als Adverben ansehen (z.B. hat uns in bellen der Parameter ja auch gesagt wie es zu tun sei) oder manchmal als direkte Objekte im Satz (beispielsweise in puts, wo der Parameter angibt, was ge-puts-t werden soll.

Lokale Variablen

Im folgenden Programm sind zwei Variablen:

def verdoppeln zahl
  ergebnis=zahl*2
  puts zahl.to_s+' verdoppeln ergibt '+ergebnis.to_s
end

verdoppeln 44
44 verdoppeln ergibt 88

Die Variablen heißen zahl und ergebnis. Beide leben innerhalb der Methode verdoppeln. Diese Variablen (und alle anderen, die du bisher gesehen hast) heißen lokale Variablen. Dies bedeutet, dass sie innerhalb der Methode leben und diese nicht verlassen können. Wenn du das versuchst, wirst du einen Fehler erhalten:

def verdoppeln zahl
  ergebnis=zahl*2
  puts zahl.to_s+' verdoppeln ergibt '+ergebnis.to_s
end

verdoppeln 44
puts ergebnis.to_s
44 verdoppeln ergibt 88
doubleThis2.rb:7: undefined local variable or method `ergebnis' for main:Object (NameError)

Undefininierte lokale Variable... in der Tat haben wir zwar die lokale Variable ergebnis definiert, aber nicht lokal dort, wo wir versuchen, sie zu verwenden; sie ist lokal in der Methode.

Das mag unpraktisch erscheinen, ist aber der Erfahrung nach sehr praktisch! Einserseits bedeutet es, dass man außerhalb einer Methode keinen Zugriff auf die lokalen Variablen der Methode hat, heißt es auch, dass die Methoden keinen Zugriff auf deine lokalen Variablen haben, und sie somit nicht zerstören können:

def kaputt var
  var=nil
  puts 'haha, ich habe deine Variable kaputt gemacht'
end

var='meine Variable ist vor dir sicher!!!'
kaputt var
puts var
haha, ich habe deine Variable kaputt gemacht
meine Variable ist vor dir sicher!!!

In diesem Programm gibt es nun zwei Variablen, die var heißen: eine innerhalb der Methode kaputt und eine außerhalb. Beim Aufruf kaputt var haben wir den String von einer Variable in die andere übergeben, dass sie beide auf denselben Inhalt zeigten. Danach hat die Methode kaputt seine lokale Variable var auf nil gesetzt, was aber nichts zu tun hatte mit unserer Variablen var außerhalb der Methode.

Rückgabewerte

Du hast sicherlich schon festgestellt, dass manche Methode etwas zurückgeben, wenn man sie aufruft. Zum Beispiel gibt gets einen String zurück (nämlich den, den du eingegeben hast) und die +-Methode in 5+3 (was ja eigentlich 5.+(3) ist) gibt 8 zurück. Arithmetische Methoden für Zahlen geben Zahlen, arithmetische Methoden für Strings geben Strings zurück.

Es ist wichtig, sich den Unterschied zu verdeutlichen zwischen einer Methode, die einen Wert zurückgibt, wenn man sie aufruft und einer Methode, die Information auf dem Bildschirm ausgibt. Halt dir vor Augen, dass 5+3 zwar 8 zurückgibt, aber nicht am Bildschirm ausgibt!

Was gibt also puts zurück? Das hat uns bisher nicht interessiert, aber schauen wir das mal genauer an:

wert = puts 'Rückgabe von puts:'
puts wert
Rückgabe von puts:
nil

Also, erstens einmal gibt puts nil zurück. Obwohl wir es nicht getestet haben, gibt auch das zweite puts nil zurück; puts gibt immer nil zurück. Jede Methode gibt etwas zurück, auch wenn es nur nil ist.

Mach hier mal eine kleine Pause, in der du rausfindest, was die Methode bellen zurück gibt.

Überrascht? So funktioniert es: zurückgegeben wird der Wert der letzten Zeile der Methode. Bei der Methode bellen gibt es also den Wert von puts 'wau ' zurück, was nil ist, weil puts immer nil liefert. Wenn wir wollen, dass diese Methode den String 'Gelbes U-Boot' zurückgibt, so müssen wir dies als letzte Zeile schreiben.

def bellen num
  puts 'wau '*num
  'gelbes U-Boot'
end

x = bellen 5
puts x
wau wau wau wau wau
gelbes U-Boot

Versuchen wir das alles man mit unserer psycholigischen Umfrage, aber diesmal mit einer Methode, die die Fragerei für uns übernimmt. Die Methode braucht also die Fragestellung als Parameter und soll true zurückgeben, wenn mit 'ja' geantwortet wurde, und false, wenn mit 'nein geantwortet wurde. (Auch wenn wir diese Antworten meist ignorieren werden ist es kein schlechter Ansatz, wenn die Methode die Antwort mal zurückgibt. Außerdem können wir die Methode auf diese Weise auch für die Bettnässer-Frage verwenden.) Ich erlaube mir ein paar Abkürzungen bei der Begrüßung und der Auswertung, aber du weißt ja schon, worum es geht, und so ist es leichter zu lesen:

def fragen fragestellung
  richtig = false
  while (not richtig)
    puts fragestellung
    antwort = gets.chomp.downcase
    if (antwort=='ja' or antwort=='nein')
      richtig=true
      if (antwort=='ja')
        antwort=true
      else
        antwort=false
      end
    else
      puts 'Bitte antworte mit "ja" oder "nein"'
    end
  end
  antwort # der Rückgabewert!!!
end

puts 'Hallo und danke, dass du ...'
puts

fragen 'Magst du Spaghetti Bolognese?'  # ignorieren!
fragen 'Magst du Maccaroni mit Käse?'

bettnaesser = fragen 'Nässt du dein Bett?'  # Antwort merken!

fragen 'Magst du Penne arrabiata?'
fragen 'Magst du Calzone?'
fragen 'Magst du Pizza Hawaii?'
fragen 'Magst du Vitello Tonnato?'
fragen 'Magst du Tira-mi-su?'

puts
puts 'Auswertung'
puts 'Danke, ...'
puts
puts bettnaesser
Hallo und danke, dass du ...

Magst du Spaghetti Bolognese?
ja
Magst du Maccaroni mit Käse?
nein
Nässt du dein Bett?
nein
Magst du Penne arrabiata?
ja
Magst du Calzone?
nein
Magst du Pizza Hawaii?
ja
Magst du Vitello Tonnato?
ja
Magst du Tira-mi-su?
ja

Auswertung
Danke, ...

false

Gar nicht schlecht, hm? Wir haben ein zusätzliche Fragen hinzugefügt (und das ist jetzt wirklich einfach) und doch ist unser Programm kürzer! Das ist ein großer fortschritt - und der Traum jedes faulen Programmierers.

Noch ein großes Beispiel

Ich denke mal, ein weiteres Beispiel für Methoden ist an dieser Stelle hilfreich. Wir nennen die Methode deutscheZahl. Sie nimmt eine Zahl (z.B. 22) und gibt das deutsche Wort dafür zurück (in diesem Fall zweiundzwanzig. Erstmal wollen wir nur Zahlen zwischen 0 und 100 betrachten.

(Hinweis: hier werden zwei neue Tricks verwendet: mit dem Schlüsselwort return kann man eine Methode frühzeitig verlassen und elsif bietet eine weitere Möglichkeit der Verzweigung. Ich denke, im Kontext wird die Bedeutung klar.)

def deutscheZahl zahl
  #  Nur Zahlen 0-100.
  if zahl < 0
    return 'Bitte gibt eine Zahl 0 oder größer ein.'
  end
  if zahl > 100
    return 'Bitte gibt eine Zahl 100 oder kleiner ein.'
  end

  zahlString = ''  #  das wird unser Ergebnis werden

  # "links" ist der Anteil von 'zahl' den wir noch verarbeiten müssen
  # "schreiben" ist der Anteil, den wir jetzt schon rausschreiben können
  links  = zahl
  schreiben = links/100           # Wie  viele  Hunderter  müssen  noch  geschrieben werden
  links  = links - schreiben*100  # Diese  Hunderter  abziehen.

  if schreiben > 0
    return 'einhundert'
  end

  schreiben = links%10  # wie viele Einer müssen wir noch schreiben?

  if links/10 %10 !=1      # Sonderfall: Teenager!!!
    links  -=schreiben     # diese Einer abziehen!

    if schreiben > 0
      if    schreiben == 1
        if links>0
          zahlString = zahlString + 'ein'
        else
          zahlString = zahlString + 'eins'
        end
      elsif schreiben == 2
        zahlString = zahlString + 'zwei'
      elsif schreiben == 3
        zahlString = zahlString + 'drei'
      elsif schreiben == 4
        zahlString = zahlString + 'vier'
      elsif schreiben == 5
        zahlString = zahlString + 'fünf'
      elsif schreiben == 6
        zahlString = zahlString + 'sechs'
      elsif schreiben == 7
        zahlString = zahlString + 'sieben'
      elsif schreiben == 8
        zahlString = zahlString + 'acht'
      elsif schreiben == 9
        zahlString = zahlString + 'neun'
      end

      if links>0
        zahlString +='und';
      end
    end
  end

  schreiben = links/10          #  wie viele Zehner müssen geschrieben werden?
  links  = links - schreiben*10  #  Diese Zehner abziehen.

  if schreiben > 0
    if schreiben == 1  #  Uh-oh...
      #  Sonderfall für die 'Teenager' 11-19
      if    links == 0
        zahlString = zahlString + 'zehn'
      elsif links == 1
        zahlString = zahlString + 'elf'
      elsif links == 2
        zahlString = zahlString + 'zwölf'
      elsif links == 3
        zahlString = zahlString + 'dreizehn'
      elsif links == 4
        zahlString = zahlString + 'vierzehn'
      elsif links == 5
        zahlString = zahlString + 'fünfzehn'
      elsif links == 6
        zahlString = zahlString + 'sechzehn'
      elsif links == 7
        zahlString = zahlString + 'siebzehn'
      elsif links == 8
        zahlString = zahlString + 'achtzehn'
      elsif links == 9
        zahlString = zahlString + 'neunzehn'
      end
      # In diesem Fall haben wir auch schon die Einer beachtet
      # und wir müssen nichts mehr schreiben
      links = 0
    elsif schreiben == 2
      zahlString = zahlString + 'zwanzig'
    elsif schreiben == 3
      zahlString = zahlString + 'dreißig'
    elsif schreiben == 4
      zahlString = zahlString + 'vierzig'
    elsif schreiben == 5
      zahlString = zahlString + 'fünfzig'
    elsif schreiben == 6
      zahlString = zahlString + 'sechzig'
    elsif schreiben == 7
      zahlString = zahlString + 'siebzig'
    elsif schreiben == 8
      zahlString = zahlString + 'achzig'
    elsif schreiben == 9
      zahlString = zahlString + 'neunzig'
    end
  end

  if zahlString == ''
    # zahlString kann nur leer sein,
    # wenn der Parameter zahl gleich 0 ist.
    return 'null'
  end

  #  Nun haben wir den zahlString erstellt
  # und müssen ihn nur noch zurück geben.
  zahlString
end

puts deutscheZahl(  0)
puts deutscheZahl(  1)
puts deutscheZahl(  9)
puts deutscheZahl( 10)
puts deutscheZahl( 11)
puts deutscheZahl( 17)
puts deutscheZahl( 32)
puts deutscheZahl( 51)
puts deutscheZahl( 88)
puts deutscheZahl( 99)
puts deutscheZahl(100)
puts deutscheZahl(555)
null
eins
neun
zehn
elf
siebzehn
zweiunddreißig
einundfünfzig
achtundachzig
neunundneunzig
einhundert
Bitte gibt eine Zahl 100 oder kleiner ein.

Da gibt's natürlich ein paar Stellen, die ich in diesem Programm nicht mag. Erst einmal gibt's zu viel Wiederholung. Außerdem werden Zahlen größer als Hundert nicht behandelt. Und drittens, gibt es zu viele Spezialfälle, zu viele returns. Wir verwenden mal ein paar Arrays und räumen etwas auf:

def deutscheZahl zahl
  # negative Zahlen: 'minus' und die positive Zahl 
  if zahl < 0
    return 'minus '+ (deutscheZahl (-1)*zahl)
    # Dies nennt man "Rekursion". 
    # Die Methode ruft sich selber mit anderen Parametern wieder auf.
    # So kann man verschiedene Fälle eleganter von einander trennen. 
  end
  if zahl == 0
    return 'null'
  end
  if zahl == 1
    return 'eins'
  end

  zahlString = ''  #  das wird unser Ergebnis werden

  # merken wir uns die deutschen Worte in Arrays, 
  # dann brauchen wir keine lange Fallunterscheidung
  einer = ['ein', 'zwei', 'drei', 'vier', 'fünf',
    'sechs', 'sieben', 'acht', 'neun']

  zehner = ['zehn', 'zwanzig', 'dreißig', 'vierzig', 'fünfzig',
    'sechzig', 'siebzig', 'achzig', 'neunzig']

  # besonderie Namen zwischen 11 und 19 
  teenager = ['elf', 'zwölf', 'dreizehn', 'vierzehn', 'fünfzehn',
        'sechzehn', 'siebzehn', 'achtzehn', 'neunzehn']

  # angenommen die Zahl ist 1000 oder kleiner...
  if zahl <1000
    h = zahl/100   #hunderter
    z = zahl/10%10 #zehner
    e = zahl%10    #einer

    if h>0
      zahlString = zahlString + einer[h-1]+'hundert'
    end

    if z==1 && e>0 #teenager
      zahlString = zahlString + teenager[e-1]
    else
      if e>0
        zahlString = zahlString + einer[e-1]
        if z>1
          zahlString = zahlString +'und'
        end
      end
      if z>0
        zahlString = zahlString +zehner[z-1]
      end
    end
    return zahlString
  end

  # nun haben wir nur Zahlen, die größer gleich 1000 sind...
  # wir betrachten immer Blöcke von drei Ziffern (also eine Zahl kleiner 1000)
  # und fügen ihnen eine Stellen-Wertigkeit an
  wert = ['', 'tausend', 'millionen', 'milliarden', 'billionen']  # das muss reichen.

  w=0
  while zahl>0
    z = zahl%1000
    if z>0
      zahlString = (deutscheZahl z) + wert[w] + zahlString
      # wieder Rekursion!
    end
    w = w+1
    zahl = zahl/1000
  end
  return zahlString
end

#Ausprobieren:
puts deutscheZahl(-17)
puts deutscheZahl( 51)
puts deutscheZahl(34555)
puts deutscheZahl(3455500000)
puts deutscheZahl(-439587000555)
puts deutscheZahl(-10000000034)
minus siebzehn
einundfünfzig
vierunddreißigtausendfünfhundertfünfundfünfzig
dreimilliardenvierhundertfünfundfünfzigmillionenfünfhunderttausend
minus vierhundertneununddreißigmilliardenfünfhundertsiebenundachzigmillionenfünfhundertfünfundfünfzig
minus zehnmilliardenvierunddreißig

Ahhh... sehr viel schöner. Das Programm ist deutlich kompakter, weshalb ich so viele Kommentare eingebaut habe. Es funktioniert auch für größere und negative Zahlen... wenn es auch Schönheitsfehler gibt: ein paar Leerzeichen oder Bindestriche würden die Lesbarkeit erhöhen und probier doch mal eine Million aus! Aber das kannst du ja jetzt selber machen...

Selber ausprobieren

  • Erweitere die Methode deutscheZahl so, dass sie auch Tausender verarbeiten kann und z.B. 'eintausend' zurückgibt, statt 'zehnhundert'.
  • Nun sollst du die Methode deutscheZahl weiter verfeinern, dass sie auch Millionen, Milliarden und Billionen zurückgeben kann.

Herzlichen Glückwunsch! Jetzt bist du ein echter Programmierer! Du hast alles gelernt, was du brauchst, um große Programme von Anfang an zu schreiben. Wenn du selber Ideen hast für Programme, die du gerne schreiben würdest, solltest du jetzt damit anfangen!

Es ist natürlich ein langsamer Prozess, wenn man alles von Anfang an neu schreibt. Und warum sollte man Code schreiben, den andere schon geschrieben haben? Willst du, dass dein Programm E-Mails versenden kann? Möchtest du Dateien auf deinem Computer speichern und laden? Wie sieht es aus mit Webseiten für ein Tutorial, in dem der Code automatisch getestet wird? ;) Ruby bietet viele verschiedene Arten von Objekten, die uns unterstützen, bessere Anwendungen schneller zu schreiben. Schauen wir uns diese Klassen an...