08. Eigene Methoden schreiben

Wie wir gesehen haben, erlauben uns Schleifen und Iteratoren, dasselbe (denselben Code) immer und immer wieder auszuführen. Manchmal wollen wir jedoch dasselbe viele Male tun, aber von verschiedenen Stellen im Programm aus. Nehmen wir zum Beispiel an, wir schreiben ein Fragebogenprogramm für einen Psychologiestudenten. Unter Berücksichtigung der Psychologiestudenten, die ich kenne, und der Fragebögen, die sie mir gegeben haben, würde es ungefähr so aussehen:

puts 'Hallo, und danke, dass Sie sich die Zeit nehmen, mir'
puts 'bei diesem Experiment zu helfen. Mein Experiment hat'
puts 'damit zu tun, wie Menschen über mexikanisches Essen denken.'
puts 'Denken Sie einfach an mexikanisches Essen und versuchen Sie,'
puts 'jede Frage ehrlich mit "ja" oder "nein" zu beantworten.'
puts 'Mein Experiment hat nichts mit Bettnässen zu tun.'
puts

# Wir stellen diese Fragen, aber wir ignorieren ihre Antworten.

guteAntwort = false
while (not guteAntwort)
  puts 'Essen Sie gerne Tacos?'
  antwort = gets.chomp.downcase
  if (antwort == 'ja' or antwort == 'nein')
    guteAntwort = true
  else
    puts 'Bitte antworten Sie mit "ja" oder "nein".'
  end
end

guteAntwort = false
while (not guteAntwort)
  puts 'Essen Sie gerne Burritos?'
  antwort = gets.chomp.downcase
  if (antwort == 'ja' or antwort == 'nein')
    guteAntwort = true
  else
    puts 'Bitte antworten Sie mit "ja" oder "nein".'
  end
end

# Wir achten jedoch auf *diese* Frage.
guteAntwort = false
while (not guteAntwort)
  puts 'Machen Sie ins Bett?'
  antwort = gets.chomp.downcase
  if (antwort == 'ja' or antwort == 'nein')
    guteAntwort = true
    if antwort == 'ja'
      bettnaesser = true
    else
      bettnaesser = false
    end
  else
    puts 'Bitte antworten Sie mit "ja" oder "nein".'
  end
end

guteAntwort = false
while (not guteAntwort)
  puts 'Essen Sie gerne Chimichangas?'
  antwort = gets.chomp.downcase
  if (antwort == 'ja' or antwort == 'nein')
    guteAntwort = true
  else
    puts 'Bitte antworten Sie mit "ja" oder "nein".'
  end
end

puts 'Nur noch ein paar Fragen...'

guteAntwort = false
while (not guteAntwort)
  puts 'Essen Sie gerne Sopapillas?'
  antwort = gets.chomp.downcase
  if (antwort == 'ja' or antwort == 'nein')
    guteAntwort = true
  else
    puts 'Bitte antworten Sie mit "ja" oder "nein".'
  end
end

# Stellen Sie viele andere Fragen über mexikanisches Essen.

puts
puts 'NACHBESPRECHUNG:'
puts 'Danke, dass Sie sich die Zeit genommen haben, bei'
puts 'diesem Experiment zu helfen. Tatsächlich hat dieses'
puts 'Experiment nichts mit mexikanischem Essen zu tun.'
puts 'Es ist ein Experiment über Bettnässen. Das mexikanische'
puts 'Essen war nur dazu da, Sie zu überrumpeln, in der'
puts 'Hoffnung, dass Sie ehrlicher antworten würden.'
puts 'Nochmals vielen Dank.'
puts
puts bettnaesser

Ein schönes langes Programm mit viel Wiederholung. (Alle Codeabschnitte rund um Fragen zu mexikanischem Essen sind identisch, und die Frage zum Bettnässen ist etwas anders.) Wiederholung ist eine schlechte Sache. Aber wir können keinen großen Iterator machen, weil wir manchmal zwischen den Fragen etwas tun wollen. In Situationen wie dieser ist es am besten, eine Methode zu schreiben. So geht's:

def sagMuh
  puts 'muuuuuuu...'
end

Äh... Unser Programm hat nicht muuuuuuu... gesagt. Warum nicht? Weil wir es ihm nicht gesagt haben. Wir haben ihm nur gesagt, wie es muuuuuuu... sagen soll, aber wir haben ihm nie gesagt, es tatsächlich zu tun. Versuchen wir es noch einmal:

def sagMuh
  puts 'muuuuuuu...'
end

sagMuh
sagMuh
puts 'quak-quak'
sagMuh
sagMuh

Ah! Viel besser. (Falls Sie kein Entisch sprechen, das war eine Ente mitten im Programm).

Also haben wir die Methode sagMuh definiert. (Methodennamen beginnen wie Variablennamen mit einem Kleinbuchstaben. Es gibt Ausnahmen, wie + oder ==). Aber müssen Methoden nicht immer mit Objekten verknüpft sein? Nun, ja, das müssen sie. Und in diesem Fall (genau wie bei puts und gets) ist die Methode nur mit dem Objekt verknüpft, das das Programm als Ganzes darstellt. Im nächsten Kapitel werden wir sehen, wie man Methoden zu anderen Objekten hinzufügt. Aber zuerst...

Methodenparameter

Sie haben vielleicht bemerkt, dass einige Methoden (wie gets oder to_s oder reverse...) einfach auf einem Objekt aufgerufen werden können. Andere Methoden (wie +, -, puts...) benötigen jedoch Parameter, um dem Objekt mitzuteilen, was es mit der Methode tun soll. Zum Beispiel sagen Sie nicht einfach 5+, richtig? Sie sagen 5, dass es addieren soll, aber Sie sagen ihm nicht, was es addieren soll.

Um sagMuh einen Parameter hinzuzufügen (die Anzahl der Muhs zum Beispiel), können wir Folgendes tun:

def sagMuh anzahlMuhs
  puts 'muuuuuuu...'*anzahlMuhs
end

sagMuh 3
puts 'oink-oink'
sagMuh  # Dies führt zu einem Fehler, da kein Parameter übergeben wurde.

anzahlMuhs ist eine Variable, die auf den übergebenen Parameter zeigt. Ich sage es noch einmal, aber es ist etwas verwirrend: anzahlMuhs ist eine Variable, die auf den übergebenen Parameter zeigt. Wenn ich also sagMuh 3 eingebe, ist der Parameter 3, und die Variable anzahlMuhs zeigt auf 3.

Wie Sie sehen können, ist der Parameter jetzt erforderlich. Schließlich soll sagMuh 'muuuuuuu' mit einer Zahl multiplizieren. Aber mit wie viel, wenn Sie es nicht gesagt haben? Ihr Computer hat keine Ahnung.

Wenn wir Objekte in Ruby mit Substantiven im Deutschen vergleichen, können Methoden ähnlich mit Verben verglichen werden. So können Sie sich Parameter als Adverbien vorstellen (wie in sagMuh, wo der Parameter uns sagt, wie man sagMuh macht) oder manchmal als direkte Objekte (wie in puts, wo der Parameter das ist, was puts drucken wird).

Lokale Variablen

Im folgenden Programm gibt es zwei Variablen:

def verdoppleDies zahl
  zahlMal2 = zahl*2
  puts 'Das Doppelte von '+zahl.to_s+' ist '+zahlMal2.to_s
end

verdoppleDies 44

Die Variablen sind zahl und zahlMal2. Beide befinden sich innerhalb der Methode verdoppleDies. Diese (und alle anderen Variablen, die Sie bisher gesehen haben) sind lokale Variablen. Das bedeutet, dass sie innerhalb der Methode leben und nicht herauskommen können. Wenn Sie es versuchen, erhalten Sie einen Fehler:

def verdoppleDies zahl
  zahlMal2 = zahl*2
  puts 'Das Doppelte von '+zahl.to_s+' ist '+zahlMal2.to_s
end

verdoppleDies 44
puts zahlMal2.to_s

Undefinierte lokale Variable... Eigentlich haben wir diese lokale Variable definiert, aber sie ist nicht lokal in Bezug auf den Ort, an dem wir versucht haben, sie zu verwenden; sie ist lokal für die Methode.

Das mag unpraktisch erscheinen, ist aber eigentlich sehr gut. Während Sie keinen Zugriff auf Variablen innerhalb von Methoden haben, bedeutet dies auch, dass niemand Zugriff auf Ihre Variablen hat, und das bedeutet, dass niemand so etwas tun kann:

def kleinePest var
  var = nil
  puts 'HAHA! Ich habe deine Variable ruiniert!'
end

var = 'Du kannst meine Variable nicht anfassen!'
kleinePest var
puts var

Es gibt tatsächlich zwei Variablen in diesem kleinen Programm namens var: eine innerhalb der Methode kleinePest und eine außerhalb. Als Sie kleinePest var aufgerufen haben, haben wir wirklich nur den String, der in var war, an die andere übergeben, sodass sie auf denselben String zeigten. Dann zeigte die Methode kleinePest ihre lokale var auf nil, aber das tat der var außerhalb der Methode nichts.

Rückgabewerte

Sie haben vielleicht bemerkt, dass einige Methoden etwas zurückgeben, wenn Sie sie aufrufen. Zum Beispiel gibt die Methode gets einen String zurück (den String, den Sie eingegeben haben), und die Methode + in 5+3 (was eigentlich 5.+(3) ist) gibt 8 zurück. Arithmetische Methoden für Zahlen geben Zahlen zurück, und arithmetische Methoden für Strings geben Strings zurück.

Es ist wichtig, den Unterschied zwischen Methoden zu verstehen, die einen Wert dorthin zurückgeben, wo er aufgerufen wurde, und Ihrem Programm, das eine Ausgabe auf Ihren Bildschirm generiert, wie es puts tut. Beachten Sie, dass 5+3 8 zurückgibt; es druckt 8 nicht auf den Bildschirm.

Was gibt puts also zurück? Wir haben uns nie darum gekümmert, aber schauen wir uns das jetzt an:

rueckgabewert = puts 'Dieses puts gab zurück:'
puts rueckgabewert

Das erste puts gab nil zurück. Obwohl wir das zweite puts nicht getestet haben, tat es dasselbe; puts gibt immer nil zurück. Jede Methode muss etwas zurückgeben, auch wenn es nur nil ist.

Machen Sie eine Pause und schreiben Sie ein Programm, das herausfindet, was die Methode sagMuh zurückgegeben hat.

Sind Sie überrascht? Nun, so funktioniert es: Der Rückgabewert einer Methode ist einfach die letzte Zeile, die in der Methode ausgewertet wurde. Im Fall der Methode sagMuh bedeutet dies, dass sie 'puts muuuuuuu...'*anzahlMuhs zurückgegeben hat, was nur nil ist, da puts immer nil zurückgibt. Wenn wir wollten, dass alle unsere Methoden den String 'yellow submarine' zurückgeben, müssten wir ihn nur ans Ende setzen:

def sagMuh anzahlMuhs
  puts 'muuuuuuu...'*anzahlMuhs
  'yellow submarine'
end

x = sagMuh 2
puts x

Versuchen wir nun noch einmal diese Psychologie-Umfrage, aber dieses Mal schreiben wir eine Methode, um die Fragen für uns zu stellen. Sie muss die Frage als Parameter nehmen und true zurückgeben, wenn die Antwort ja war, und false, wenn die Antwort nein war. (Auch wenn wir die Antwort die meiste Zeit ignorieren, ist es eine gute Idee, die Methode die Antwort zurückgeben zu lassen. Auf diese Weise können wir sie für die Frage zum Bettnässen verwenden). Ich werde auch die Begrüßung und die Nachbesprechung verkürzen, nur um das Lesen zu erleichtern:

def fragen frage
  guteAntwort = false
  while (not guteAntwort)
    puts frage
    antwort = gets.chomp.downcase

    if (antwort == 'ja' or antwort == 'nein')
      guteAntwort = true
      if antwort == 'ja'
        dieAntwort = true
      else
        dieAntwort = false
      end
    else
      puts 'Bitte antworten Sie mit "ja" oder "nein".'
    end
  end

  dieAntwort # Das ist es, was wir zurückgeben (true oder false).
end

puts 'Hallo, und danke für...'
puts

fragen 'Essen Sie gerne Tacos?'      # Wir ignorieren diesen Rückgabewert.
fragen 'Essen Sie gerne Burritos?'
bettnaesser = fragen 'Machen Sie ins Bett?'  # Wir speichern diesen Rückgabewert.
fragen 'Essen Sie gerne Chimichangas?'
fragen 'Essen Sie gerne Sopapillas?'
fragen 'Essen Sie gerne Tamales?'
puts 'Nur noch ein paar Fragen...'
fragen 'Trinken Sie gerne Horchata?'
fragen 'Essen Sie gerne Flautas?'

puts
puts 'NACHBESPRECHUNG:'
puts 'Danke für...'
puts
puts bettnaesser

Nicht schlecht, oder? Wir können mehr Fragen hinzufügen (und mehr Fragen hinzuzufügen ist jetzt einfach), aber unser Programm bleibt kurz! Das ist ein riesiger Fortschritt – der Traum eines jeden faulen Programmierers.

Ein weiteres großes Beispiel

Ich denke, eine weitere Beispielmethode wäre hier sehr nützlich. Nennen wir diese deutscheNummer. Diese Methode nimmt eine Zahl, wie 22, und gibt die deutsche Version davon zurück (in diesem Fall den String 'zweiundzwanzig'). Beschränken wir es vorerst auf ganze Zahlen zwischen 0 und 100.

(HINWEIS: Diese Methode verwendet einen neuen Trick, um vorzeitig mit dem Schlüsselwort return aus einer Methode zurückzukehren, und führt ein neues Konzept ein: elsif. Dies sollte im Kontext klar sein).

def deutscheNummer nummer
  # Wir wollen nur Zahlen zwischen 0 und 100.
  if nummer < 0
    return 'Bitte geben Sie eine Zahl ein, die nicht negativ ist.'
  end
  if nummer > 100
    return 'Bitte geben Sie eine Zahl ein, die 100 oder weniger ist.'
  end

  numString = ''  # Das ist der String, den wir zurückgeben werden.

  # "rest" ist, wie viel von der Zahl wir noch ausschreiben müssen.
  # "schreiben" ist der Teil, den wir gerade ausschreiben.
  rest      = nummer
  schreiben = rest/100          # Wie viele Hunderter müssen noch geschrieben werden?
  rest      = rest - schreiben*100  # Ziehen Sie diese Hunderter ab.

  if schreiben > 0
    return 'einhundert'
  end

  schreiben = rest/10           # Wie viele Zehner müssen noch geschrieben werden?
  rest      = rest - schreiben*10   # Ziehen Sie diese Zehner ab.

  if schreiben > 0
    if ((schreiben == 1) and (rest > 0))
      # Da wir nicht "zehn-zwei" statt "zwölf" schreiben können,
      # müssen wir für diese eine besondere Ausnahme machen.
      if rest == 1
        numString = numString + 'elf'
      elsif rest == 2
        numString = numString + 'zwölf'
      elsif rest == 3
        numString = numString + 'dreizehn'
      elsif rest == 4
        numString = numString + 'vierzehn'
      elsif rest == 5
        numString = numString + 'fünfzehn'
      elsif rest == 6
        numString = numString + 'sechzehn'
      elsif rest == 7
        numString = numString + 'siebzehn'
      elsif rest == 8
        numString = numString + 'achtzehn'
      elsif rest == 9
        numString = numString + 'neunzehn'
      end
      # Da wir uns bereits um die Ziffer an der Einerstelle gekümmert haben,
      # haben wir nichts mehr zu schreiben.
      rest = 0
    elsif schreiben == 1
      numString = numString + 'zehn'
    elsif schreiben == 2
      # Im Deutschen kommen die Einer vor den Zehnern bei Zahlen > 20
      if rest > 0
        if rest == 1
           numString = numString + 'ein'
        elsif rest == 2
           numString = numString + 'zwei'
        # ... (vereinfacht für dieses Beispiel ohne Arrays)
        end
        numString = numString + 'und'
      end
      numString = numString + 'zwanzig'
      rest = 0 # Einer wurden schon behandelt
    # ... und so weiter für andere Zehner
    end
  end

  schreiben = rest  # Wie viele Einer müssen noch geschrieben werden?
  rest      = 0     # Ziehen Sie diese Einer ab.

  if schreiben > 0
    if schreiben == 1
      numString = numString + 'eins'
    elsif schreiben == 2
      numString = numString + 'zwei'
    elsif schreiben == 3
      numString = numString + 'drei'
    # ...
    end
  end

  if numString == ''
    # Die einzige Möglichkeit, dass "numString" leer sein könnte, ist, wenn
    # "nummer" 0 ist.
    return 'null'
  end

  numString
end

Nun, es gibt ein paar Dinge, die ich an diesem Programm nicht mag. Erstens: Es gibt zu viel Wiederholung. Zweitens: Es behandelt keine Zahlen größer als 100. Drittens: Es gibt zu viele Sonderfälle, zu viele returns. Und im Deutschen ist die Reihenfolge von Einer und Zehner vertauscht ("einundzwanzig"), was es komplizierter macht. Lassen Sie uns einige Arrays verwenden und versuchen, es aufzuräumen:

def deutscheNummer nummer
  if nummer < 0  # Keine negativen Zahlen.
    return 'Bitte geben Sie eine Zahl ein, die nicht negativ ist.'
  end
  if nummer == 0
    return 'null'
  end

  # Keine Sonderfälle mehr! Keine Returns mehr!

  numString = ''  # Das ist der String, den wir zurückgeben werden.

  einer        = ['eins',    'zwei',      'drei',     'vier',      'fünf',
                  'sechs',   'sieben',    'acht',     'neun']
  zehner       = ['zehn',    'zwanzig',   'dreißig',  'vierzig',   'fünfzig',
                  'sechzig', 'siebzig',   'achtzig',  'neunzig']
  teenager     = ['elf',     'zwölf',     'dreizehn', 'vierzehn',  'fünfzehn',
                  'sechzehn','siebzehn',  'achtzehn', 'neunzehn']

  # "rest" ist, wie viel von der Zahl wir noch ausschreiben müssen.
  # "schreiben" ist der Teil, den wir gerade ausschreiben.
  rest      = nummer
  schreiben = rest/100          # Wie viele Hunderter müssen noch geschrieben werden?
  rest      = rest - schreiben*100  # Ziehen Sie diese Hunderter ab.

  if schreiben > 0
    # Jetzt hier ein wirklich schlauer Trick:
    hunderter = deutscheNummer schreiben
    numString = numString + hunderter + ' hundert'
    # Das nennt man "Rekursion". Also, was habe ich gerade getan?
    # Ich habe dieser Methode gesagt, sie soll sich selbst aufrufen, aber mit "schreiben" statt
    # "nummer". Denken Sie daran, dass "schreiben" (im Moment) die Anzahl der
    # Hunderter ist, die wir ausschreiben müssen.

    if rest > 0
      # Damit wir nicht 'zweihundertfünfundfünfzig'... schreiben
      numString = numString + ' '
    end
  end

  schreiben = rest/10           # Wie viele Zehner müssen noch geschrieben werden?
  rest      = rest - schreiben*10   # Ziehen Sie diese Zehner ab.

  if schreiben > 0
    if ((schreiben == 1) and (rest > 0))
      # Da wir nicht "zehn-zwei" statt "zwölf" schreiben können,
      # müssen wir für diese eine besondere Ausnahme machen.
      numString = numString + teenager[rest-1]
      # Das "-1" ist, weil teenager[3] 'vierzehn' ist, nicht 'dreizehn'.

      # Da wir uns bereits um die Ziffer an der Einerstelle gekümmert haben,
      # haben wir nichts mehr zu schreiben.
      rest = 0
    else
      # Für Deutsch müssen wir bei > 20 erst die Einer, dann "und", dann die Zehner schreiben.
      if rest > 0
        # Achtung: "eins" vs "ein" (bei einundzwanzig).
        # Wir machen es hier einfach und benutzen das Array.
        einheit = einer[rest-1]
        if rest == 1
            einheit = 'ein'
        end
        numString = numString + einheit + 'und'
      end

      numString = numString + zehner[schreiben-1]
      
      # Da wir die Einer schon vor den Zehnern geschrieben haben:
      rest = 0 
    end
  end

  schreiben = rest  # Wie viele Einer müssen noch geschrieben werden?
  rest      = 0     # Ziehen Sie diese Einer ab.

  if schreiben > 0
    numString = numString + einer[schreiben-1]
  end

  # Jetzt geben wir einfach "numString" zurück...
  numString
end

puts deutscheNummer(  0)
puts deutscheNummer(  9)
puts deutscheNummer( 10)
puts deutscheNummer( 11)
puts deutscheNummer( 17)
puts deutscheNummer( 32)
puts deutscheNummer( 88)
puts deutscheNummer( 99)
puts deutscheNummer(100)
puts deutscheNummer(101)
puts deutscheNummer(234)
puts deutscheNummer(3211)
puts deutscheNummer(999999)
puts deutscheNummer(1000000000000)

Ahhhh.... Das ist viel besser. Das Programm ist ziemlich dicht, weshalb ich so viele Kommentare eingefügt habe. Es funktioniert sogar für große Zahlen... wenn auch nicht ganz so schön, wie man hoffen könnte. Zum Beispiel denke ich, dass 'eine Billion' ein schönerer Rückgabewert für diese letzte Zahl wäre, oder sogar 'eine Million Millionen'. Tatsächlich können Sie das jetzt tun...

Ein paar Dinge zum Ausprobieren

  • Verbessern Sie deutscheNummer. Fügen Sie zuerst Tausende ein. Es sollte also 'eintausend' statt 'zehn hundert' zurückgeben.
  • Erweitern Sie deutscheNummer noch etwas mehr. Fügen Sie jetzt Millionen ein. Versuchen Sie dann, Milliarden und Billionen hinzuzufügen. Wie hoch können Sie gehen?
  • "Neunundneunzig Flaschen Bier..." Schreiben Sie mit deutscheNummer und Ihrem alten Programm den Text zu diesem Lied diesmal auf die richtige Weise. Bestrafen Sie Ihren Computer: Lassen Sie ihn bei 9999 beginnen.

Herzlichen Glückwunsch! An diesem Punkt sind Sie wirklich ein Programmierer! Sie haben alles gelernt, was Sie brauchen, um riesige Programme von Grund auf neu zu schreiben. Wenn Sie Ideen für Programme haben, die Sie für sich selbst schreiben möchten, probieren Sie es aus!

Natürlich kann es ein langsamer Prozess sein, alles von Grund auf neu zu bauen. Warum Zeit damit verbringen, Code zu schreiben, den schon jemand anderes geschrieben hat? Sie wollen eine E-Mail senden? Sie wollen Dateien auf Ihrem Computer speichern und laden? Wie wäre es mit dem Generieren von Webseiten für ein Tutorial, bei dem die Codebeispiele tatsächlich jedes Mal ausgeführt werden, wenn die Seite geladen wird? :) Ruby hat viele verschiedene Arten von Objekten, die wir verwenden können, um Programme besser und schneller zu schreiben.