Ruby-Programmierung: Methoden

Zurück zum Inhaltsverzeichnis.

Der Sinn einer Programmiersprache besteht darin die Möglichkeiten, aber auch Begrenzungen, des Computers möglichst weit zu abstrahieren. So wird es dem Entwickler möglich seinen Quelltext nah am Problem zu gestalten, statt sich mit der Technik und Funktionsweise des Computers beschäftigen zu müssen. Eine erste Form der Abstraktion sind Methoden. Betrachtet man zum Beispiel die Funktion puts so wissen wir, dass sie auf der Kommandozeile eine Ausgabe tätigt. Es ist nicht wichtig, wie die Methode eine Funktion bereit stellt, sondern nur was sie ermöglicht. In diesem Kapitel geht es um die Definition eigener Methoden.

Methoden

Bearbeiten
def hallo
  puts "Hallo Welt!"
end

hallo

Methodendefinitionen werden mit dem Schlüsselwort def eingeleitet und der Block wie im Falle der Kontrollstrukturen mit end beendet, alles was zwischen diesen beiden Zeilen steht wird beim Aufrufen der Funktion ausgeführt, es ist also ohne Probleme möglich mehrzeilige Methoden zu schreiben. Durch einfaches Schreiben des Methodennamens in einem Programmtext, wird diese an der Stelle aufgerufen und ausgeführt.

Bei Methodennamen gelten ähnliche Regeln wie bei Variablennamen. Zusätzlich ist die Konvention der Methodennamen möglichst nah an der Funktion zu wählen und ggf. eine Warnung mitzugeben. Eine Warnung ist beispielsweise dann nötig, wenn die Funktion die übergebenen Parameter verändert, oder Daten auf der Festplatte verändert. Dieses Verhalten wird auch als Seiteneffekte bezeichnet und wird mit einem Ausrufezeichen am Ende des Methodennamens angezeigt. Eine Prüfung endet mit einem Fragezeichen.

def is_two? (zahl)
  puts "Ja!" if zahl == 2
  puts "Nein!" unless zahl == 2
end

Parameter

Bearbeiten

Bleiben wir bei der Betrachtung der Methode is_two?, so haben wir bereits beim ersten Programm festgestellt, dass sie einen Parameter erhält, nämlich die Zahl, die auf die Gleichheit mit 2 geprüft werden soll. Methoden können entsprechend der übergebenen Parameter auch unterschiedliche Funktionen enthalten (zum Beispiel: +).

def hallo(ort)
  puts "Hallo " + ort    
end
hallo "Welt"

Hier wird der Methode ein String mit dem Wert "Welt" übergeben. Die Methode gibt daraufhin den Text "Hallo Welt" aus. Die Verwendung von Klammern bei Parametern einer Methode ist in den meisten Fällen optional. In diesem Buch werden bei Methodendefinitionen Klammern verwendet. Beim Aufruf der Methode werden sie je nach Anzahl der Parameter weggelassen.

Es ist auch möglich, den Parametern von Methoden Standardwerte zuzuteilen. Im nachfolgenden Beispiel wird der Variablen ort der Wert "Welt" zugeteilt. Wenn man die Funktion mit Parameter aufruft, dann überschreibt der übergebene Parameter den Standardwert. Im nachfolgenden Skript wird der Parameter ort mit "Erde" initialisiert, dadurch ist es möglich die Methode ohne Parameter aufzrufen, wodurch "Hallo Erde" ausgegeben wird, oder diesen Parameter wie oben manuell zu setzen.

def hallo(ort = "Erde") 
  puts "Hallo " + ort
end

Rückgabewerte

Bearbeiten

Wenn man sich klarmacht, dass Programmiersprachen in den Anfängen vor allem von Mathematikern entwickeln wurden, dann wird deutlich warum Methoden (in anderen Programmiersprachen auch Funktionen) Gemeinsamkeiten mit den mathematischen Funktionen haben. Eine mathematische Funktion erhält Parameter in Form von Variablen und gibt einen Wert (oder mehrere Werte) zurück. Auch Methoden müssen also Rückgabewerte ermöglichen. In Ruby gibt es zwei Möglichkeiten, dass Methoden Werte zurückgeben. Im ersten und einfachsten Fall ist der Rückgabewert der letzte errechnete Wert vor dem Verlassen der Funktion. Im zweiten Fall kann man mit dem Schlüsselwort return anzeigen, dass man sowohl die Methoden verlässt und der Wert nach return zurückgegeben werden soll.

def addiere(a, b)
  a + b
end

# Ist das Gleiche wie:
def addiere(a, b)
  return a + b
end

Mehrere Quelltextdateien

Bearbeiten

Bei längeren Programmtexten wird es auch in einer gut lesbaren Sprache wie Ruby notwendig, seinen Quelltext zu strukturieren. Daher ist es oftmals sinnvoll, logisch zusammenhängende Bereiche eines Programms in einer eigenen Quelltextdatei zu schreiben. Das Einbinden erfolgt dann mit dem Schlüsselwort require. Standardmäßig sucht require in dem Ordner, indem auch Rubygems seine Erweiterungen speichert, dies kann jedoch durch angabe eines konkreten Pfades zur Datei geändert werden. Will man stattdessen einen Pfad relativ zur aufrufenden Datei angeben so verwendet man require_relative, das mit Ruby 1.9 eingeführt wurde. Im Gegensatz zu require ist es deutlich flexibler. Im unten stehenden Beispiel führt zwar ein require './hallo2.rb' zu einem funktionierenden Programm, doch nur, wenn das Skript aus demselben Verzeichnis aufgerufen wird, da der Punkt in der Pfadangabe nicht relativ zum Skript ausgewertet wird, sondern für das aktuelle Arbeitsverzeichnis steht. Außer zur Verwendung von Gems sollte man auf require verzichten.

#hallo1.rb
require_relative 'hallo2.rb'
hallo

#hallo2.rb
def hallo
  puts 'Hallo Welt!'
end

Speichert man beide Skripte im gleichen Verzeichnis ab und startet hallo1.rb, dann führt es zur Ausgabe von Hallo Welt!. Das Skript importiert zunächst alle Definitionen aus dem Skript hallo2.rb, in diesem Fall also die Definition einer Methode hallo, die nun in hallo1.rb genauso zur Verfügung steht, als wäre sie dort definiert.

Rekursion und Sichtbarkeit von Variablen

Bearbeiten

Rekursion bezeichnet einen Vorgang, dass sich innerhalb einer Methode ein weiterer Methodenaufruf der selben Methoden befindet. Mit dieser insbesondere in funktionalen Programmiersprachen beliebten Möglichkeit lassen sich einige Probleme eleganter formulieren, als es mit Schleifen möglich wäre. Bevor wir jedoch eine einfache rekursive Methode betrachten, ist es nötig, sich die Sichtbarkeit von Variablen anzuschauen.

def n_wird_zwei(n)
  n = 2
end
n = 1
$n = 3
n_wird_zwei n
n_wird_zwei $n
puts n
puts $n

Entgegen der ersten Annahme gibt dieses Programm erst 1, dann jedoch 2 aus. Das hat zum einen etwas mit der Sichtbarkeit von Variablen zu tun. Beim Aufrufen der Funktion wird nicht die tatsächliche Variable übergeben, sondern erst kopiert und dann übergeben. Von dieser Regel ausgenommen sind globale Variablen, daher verändert die Methode nicht die lokale Variable n, sondern die globale Variable $n. Das ist der Grund, warum die oben erwähnte Namenskonvention erst bei der späteren, genaueren Betrachtung von Methoden wichtig wird. Die Methode n_wird_zwei verändert also nicht die übergebene Variable, sondern gibt stattdessen den Wert 2 zurück. Erinnern wir uns an die eingangs erwähnte Rekursion, dann ist dieses Verhalten von übergebenen Parametern sehr wünschenswert, denn wir können innerhalb einer Methode diese wieder (und auch andere Methoden) aufrufen ohne erst unsere Variablen in temporären Variablen zu sichern. Folgende sehr einfache Rekursion zeigt das Herunterzählen von 10 bis 0.

def decrease(n)
  return if n < 0
  puts n
  decrease n-1
end

decrease 10

Dieses Beispiel hätte man natürlich auch mit einer Schleife lösen können. Das ist ein weit verbreitetes Problem beim Lehren von Rekursionen, denn die einfachen Bespiele sind viel einfacher durch Schleifen darstellbar und wirken daher überflüssig. Erinnert man sich jedoch an den einleitenden Text dieser Seite, so geht es bei der Frage, ob man Rekursion oder Schleifen benutzt lediglich darum, welche Implementierung näher am Problem ist.

Rückblick: Fakultät

Bearbeiten

Mit dem Wissen aus diesem Kapitel ist es nun möglich die, auf der vorherigen Seite vorgestellte, Fakultätsfunktion rekursiv zu implementieren.

Mit der Rekursionsformel   und der Abbruchbedingung   ausgedrückt, lautet unsere Methode in Code ausgedrückt:

# fakul.rb
# Berechnet die Fakultät.

def fakul(n)
  if n == 0
    1               # 0! = 1
  else
    n * fakul(n-1)  # n! = n*(n-1)!
  end
end

puts fakul(5)

Ob eine Schleife oder Rekursion sinnvoller ist, ist häufig eine Stilfrage. Oftmals ist eine rekursive Formulierung übersichtlicher, kann aber langsamer sein, als eine äquivalente Formulierung mit einer Schleife. In Ruby, sowie auch vielen anderen Programmiersprachen, gibt es eine maximale Rekursionstiefe.[Bem 1] Das heißt nach einer bestimmten Anzahl verschachtelter Methodenaufrufe wird die Programmausführung abgebrochen und man erhält den Fehler "SystemStackError: stack level too deep". Wenn man im obigen Beispiel eine große Zahl für n einsetzt, kann man den Fehler reproduzieren. Als Faustregel sollte man in Ruby Rekursion vermeiden, wenn die Rekursionstiefe groß ist, und Rekursion verwenden, wenn man dadurch komplizierten Code vereinfachen kann.

Bemerkungen

Bearbeiten
  1. Präziser müsste man von "maximaler Stack-Tiefe" sprechen. Der Stack hat unter anderem die Aufgabe die lokalen Variablen zu speichern und sich zu merken, welche Methoden von wo aus aufgerufen wurden und wohin die Ausführung zurückspringt, wenn die Methode fertig ausgeführt wurde. Das Problem ist nicht die Rekursion einer einzigen Methode, sondern ganz allgemein, wenn es zuviele verschachtelte Methodenaufrufe gibt. Wenn man keine Rekursion verwendet oder die maximale Rekursionstiefe einer Methode einschränkt, wird man in aller Regel nicht auf dieses Problem stoßen.