Letztes Update: 12.12.2017
Behandelte Befehle: Funktionsdefinition, Parameter, Rückgabewert, Überladen, Signatur, Skopus

Funktionen sind neben Variablen und Kontrollfluss-Strukturen (if, while, for) der dritte Pfeiler Ihrer Programmierausbildung. Mit Funktionen wird Ihr Code strukturierter und lesbarer. Noch wichtiger: Teile Ihres Codes werden wiederverwendbar.

9.1 Was ist eine Funktion?

Stellen Sie sich vor, Sie könnten ein paar Zeilen Code in ein Paket packen und ein Schild mit foo aufs Paket kleben. Jedesmal, wenn Sie diese Zeilen in Ihrem Code ausführen wollen, brauchen Sie nur foo hinschreiben. foo ist eine Funktion!

Sie haben übrigens schon Funktionen benutzt, in der Form von "Befehlen" wie size(), ellipse() und rect() sind Funktionen, die das Processing-Team für Sie geschrieben hat. In diesem Kapitel lernen Sie, wie Sie selbst solche Befehle bereitstellen.

Video: Funktionen 1 (5:02)

Funktion definieren

Bevor Sie eine neue Funktion verwenden können, müssen Sie Processing sagen, was diese Funktion genau tun soll. Das machen Sie mit der Definition der Funktion.

Allgemein definieren Sie eine (einfache) Funktion wie folgt:

void NAME() {
   CODE
}

Wobei CODE für eine beliebige Anzahl von Codezeilen stehen kann. Diese Funktion hier zeichnet zum Beispiel ein Gesicht mit zwei schwarzen Knopfaugen an der Stelle (x, y), wobei x und y bereits oben im Code deklariert sein müssen:

void zeichneGesicht() {
   fill(255); // weiß
   ellipse(x, y, 80, 80); // Gesicht
   fill(0); // schwarz
   ellipse(x-15, y-15, 20, 20); // Auge
   ellipse(x+15, y-15, 20, 20); // Auge
}

Achten Sie darauf, Funktionen immer klein zu schreiben. Funktionen können nur im aktiven Modus definiert werden, d.h. Sie müssen entweder setup() oder draw() in Ihrem Code haben.

Etwas merkwürdig sehen noch die runden Klammern hinter dem Funktionsnamen aus, diese bekommen im Abschnitt Parameter Sinn. Als nächstes erfahren Sie, wie Sie Ihre neue Funktion denn verwenden.

Funktion aufrufen

Wir haben oben die Funktion zeichneGesicht definiert. Diese steht uns ab sofort als Befehl zur Verfügung, genauso wie die "eingebauten" Befehle ellipse, size, stroke usw.

Sie rufen den Befehl dann so auf:

zeichneGesicht();

Sehen wir uns das vollständige Programm an:

int x = 50;
int y = 50;

void setup() { // nichts zu tun
}

void draw() {
   // sehr übersichtlich :)
   background(255);
   zeichneGesicht(); // Funktionsaufruf
   x++;
}

// Funktionsdefinition:
void zeichneGesicht() {
   fill(255);
   ellipse(x, y, 80, 80);
   fill(0);
   ellipse(x-15, y-15, 20, 20);
   ellipse(x+15, y-15, 20, 20);
}

Der Vorteil hier ist zunächst mal, dass Ihr Programm übersichtlicher wird. Wenn man in draw() hineinschaut, sieht man direkt, dass offenbar etwas entlang der x-Achse bewegt wird. Der Name der Funktion zeichneGesicht sagt klar, was bei Aufruf des Befehls zu erwarten ist. Das heißt: Funktionen helfen, Code übersichtlicher und lesbarer zu gestalten. Man spricht auch von selbsterklärendem Code, d.h. der Code spricht für sich selbst und muss nicht (oder kaum) durch Kommentare ergänzt werden. Dabei ist wichtig, aussagekräftige Namen für die Funktionen zu wählen.

Übungsaufgaben

(a) Hello, world!

Schreiben Sie die Funktion hello(), die einfach "hello, world" auf die Konsole schreibt.

Hinweis: Denken Sie daran, dass Sie ein setup() oder draw() benötigen, um Funktionen definieren zu dürfen.

Testen Sie Ihren Code mit:

void setup() {
  hello();
  hello();
  hello();
}

Sie sollten sehen:

hello, world
hello, world
hello, world

(b) Mauszeiger

Schreiben Sie eine Funktion mauszeiger(), welche an Position der Maus einen Kreis mit Durchmesser 20 zeichnet.

Testen Sie Ihren Code mit

void draw() {
  background(255);
  mauszeiger();
}

(c) Code durch Funktion ersetzen

Sie sollen folgenden Code "aufräumen". Im Code werden ein Haus und eine Sonne gezeichnet. Definieren Sie eine neue Funktion zeichneHaus(), in dem das Haus gezeichnet wird und rufen Sie diese Funktion in draw() auf. Tun Sie das gleiche für die Sonne mit einer Funktion zeichneSonne().

void setup() {
  size(300, 300);
}

void draw() {
  background(#31F0FF);
  noStroke();
  fill(#FF3134);
  rect(80, 200, 80, 80);
  triangle(80, 200, 160, 200, 120, 150);
  fill(#FFF931);
  ellipse(200, 100, 80, 80);
}

(d) Dreierlei

Schreiben Sie drei Funktionen one(), two(), three(). Die Funktionen geben jeweils die Zahl 1, 2 und 3 auf der Konsole aus.

Rufen Sie in setup() die Funktion one() auf.

In der Funktion one() rufen Sie two() auf.

In der Funktion two() rufen Sie three() auf.

Testen Sie Ihren Code unbedingt genau mit:

void setup() {
  one();
}

Sie sollten sehen:

1
2
3

Wenn Sie Ihren Code wie folgt testen, haben Sie was falsch gemacht. (Lesen Sie nochmal die Aufgabenbeschreibung...)

void setup() {
  one();
  two();
  three();
}

Zusammenfassung

Sie können eigene Funktionen definieren, die Sie dann im Code beliebig oft aufrufen können. Allgemein sieht eine Funktionsdefinition so aus:

void NAME() {
  CODE
}

Funktionen dienen dazu, den Code übersichtlicher und lesbarer zu gestalten. Dazu sollten die gewählten Funktionsnamen möglichst aussagekräftig sein.

Eigene Funktionen kann man nur im aktiven Modus definieren.

Funktionsnamen schreibt man immer klein.

9.2 Parameter

Sie haben oben nur die einfachste Variante einer Funktion gesehen. Wenn Sie sich den Befehl rect ansehen, dann bekommt der Befehl noch einige Infos mit auf den Weg, nämlich Eckpunkt, Breite und Höhe:

rect(50, 50, 100, 80);

Diese Zusatzinformationen nennt man Parameter. Parameter geben Ihnen die Möglichkeit, viele Varianten einer Aufgabe ("zeichne ein Rechteck!") mit der selben Funktion (rect) zu erfassen, indem die jeweils spezifische Ausprägung (Position x:50 y:50, Breite 100, Höhe 80) in den Parametern gesetzt wird.

Video: Funktionen 2 - Parameter (6:11)

Parameter in der Funktionsdefinition

Damit Ihre Funktion Parameter entgegennehmen kann, müssen Sie diese bei der Funktionsdefinition mitdefinieren. Zum Beispiel wollen Sie der Gesichtsfunktion die Augenfarbe mitgeben:

void zeichneGesicht(int augenfarbe) {
   fill(255); // weiß
   ellipse(x, y, 80, 80); // Gesicht
   fill(augenfarbe);
   ellipse(x-15, y-15, 20, 20); // Auge
   ellipse(x+15, y-15, 20, 20); // Auge
}

Sie sehen, dass Sie in den Klammern zwei Dinge hinzufügen: den Typ des Parameters und den Namen. Im Grunde ist das exakt das gleiche wie bei einer Variable, die Sie ganz oben im Code deklarieren.

Und tatsächlich: der Parameter augenfarbe ist eine Variable.

Parameter ist lokale Variable

Ein Parameter ist eine lokale Variable innerhalb der Funktion. Das heißt, dass diese Variable eine begrenzte Lebensdauer hat. Sie beginnt bei der öffnenden geschweiften Klammer und endet bei der schließenden. Außerhalb des Funktionskörpers kann auf Parameter nicht zugegriffen werden.

Sie können - durch Komma getrennt - mehrere Parameter definieren, z.B. die Farbe von sowohl Gesicht als auch Augen von außen bestimmen lassen:

void zeichneGesicht2(int gesichtsfarbe, int augenfarbe) {
   fill(gesichtsfarbe);
   ellipse(x, y, 80, 80); // Gesicht
   fill(augenfarbe);
   ellipse(x-15, y-15, 20, 20); // Auge
   ellipse(x+15, y-15, 20, 20); // Auge
}

Die allgemeine Form der Funktionsdefinition mit Parametern ist:

void NAME(TYP1 PARAM1, TYP2 PARAM2, ...) {
   CODE
}

Im folgenden Beispiel sind die Parameter a und b nur im Code der Funktion mittelwert gültig.

void setup() {
  println(mittelwert(5.0, 12.5));
}

float mittelwert(float a, float b) {
  float summe = a + b;
  return summe / 2;
}

Jeder Versuch, die Parameter a und b z.B. in setup() zu verwenden, wird mit einer Fehlermeldung bestraft, denn die Variablen existieren nur innerhalb der Funktionsdefinition.

void setup() {
  println(mittelwert(5.0, 12.5));
  println(a); // Fehler! Variable a ist hier nicht gültig.
}

float mittelwert(float a, float b) {
  float summe = a + b;
  return summe / 2;
}

Parameter nicht verändern

WICHTIG: Verändern Sie niemals einen Parameter innerhalb einer Funktion! Dies ist zwar möglich, aber schlechter Stil, da bei längerem Code unklar ist, ob der Parameter noch den ursprünglichen Wert hat.

// Schlechter Stil!

void zeichneFigur(int x, int y) {
  rect(x, y, 20, 40);
  x = x + 10; // Parameter x wird verändert!
  y = y - 10; // Parameter y wird verändert!
  ellipse(x, y, 20, 20);
}

Führen Sie stattdessen jeweils eine neue lokale Variable ein:

// Stattdessen lokale Variablen:

void zeichneKreis(int x, int y) {
  rect(x, y, 20, 40);
  int x2 = x + 10; // OK
  int y2 = y - 10; // OK
  ellipse(x2, y2, 20, 20);
}

Diese Regel gilt nur für primitive Datentypen (int, float, boolean ...). Bei Objekten (z.B. PVector) und Arrays (sind intern Objekte) kann es sein, dass man will, dass ein Parameter verändert wird. Man sollte dies aber sehr bewusst und mit Vorsicht tun.

Aufruf mit Parametern

Beim Aufruf einer Funktion mit Parametern, müssen Sie darauf achten, dass Sie genau die Information mitgeben, die Ihre Definition verlangt. Das heißt, die Anzahl, Reihenfolge und Datentypen der Parameter müssen mit den Angaben Ihrer Funktionsdefinition übereinstimmten.

Diese Aufrufe sind falsch:

zeichneGesicht(36.5); // falscher Typ, muss int sein
zeichneGesicht(50, 100); // falsche Anzahl, nur 1 Param.
zeichneGesicht(50, "hallo"); // falsche Anzahl, falscher Typ

So ist es richtig:

zeichneGesicht(0);
zeichneGesichtZwei(255, 0);

Beachten Sie, dass Sie beim Funktionsaufruf natürlich auch Variablen in den Parametern verwenden können, z.B.

int farbe1 = 0;
int farbe2 = 255;

zeichneGesicht(farbe1);
zeichneGesichtZwei(farbe1, farbe2);

Übungsaufgaben

(a) Plus eins

Schreiben Sie die Funktion plusEins, die als Parameter eine Zahl x als int bekommt und x+1 auf die Konsole schreibt.

Der Aufruf plusEins(5); würde also folgendes produzieren:

6

(b) Summe ausgeben

Schreiben Sie eine Funktion printSum, die drei Parameter vom Typ float bekommt und die Summe dieser drei übergebenen Zahlen auf die Konsole schreibt.

Der Aufruf printSum(3, 2, 5); würde also folgendes produzieren:

10.0

(c) Sterne sehen

Schreiben Sie eine Funktion sternchen, die als Parameter eine Zahl n bekommt und dann n Sternchen auf der Konsole ausgibt.

Der Aufruf sternchen(5); würde folgendes ausgeben:

*****

Hinweis: Verwenden Sie eine Schleife.

(d) Mauszeiger II

Schreiben Sie eine Funktion mauszeiger, mit fünf Parametern: x, y, r, g, b. Die Funktion zeichnet einen Kreis an der Position (x, y) mit der Farbe (r, g, b) gefüllt.

Testen Sie Ihren Code mit

void draw() {
  background(255);
  mauszeiger(mouseX, mouseY, 255, 0, 0);
}

Sie sollten einen rot gefüllten Kreis sehen, der der Maus folgt.

(e) Begrüßung

Schreiben Sie die Funktion greeting(), die einen Parameter name vom Typ String bekommt und dann "Servus, <name>" ausgibt.

Zum Beispiel beim Aufruf von

greeting("Helmut");

Sehen Sie:

Servus, Helmut

Zusammenfassung

Sie können einer Funktion Parameter geben - das sind Informationen, die das Verhalten der Funktion bestimmen, z.B. Farbwerte oder Koordinaten.

In der Definition der Funktion geben Sie den Parametern Namen und nennen den Typ. Im Prinzip deklarieren Sie hier eine Reihe von lokalen Variablen:

void NAME(TYP1 PARAM1, TYP2 PARAM2, ...) {
  CODE
}

Beim Aufruf einer Funktion müssen Sie die konkreten Parameterwerte angeben (hierbei dürfen natürlich auch Variablen im Spiel sein). Dabei muss die Anzahl, Reihenfolge und Datentypen der Parameter mit den Angaben Ihrer Funktionsdefinition übereinstimmten.

Sie kennen Funktionsaufrufe zur Genüge - jede Zeile des folgenden Codes ruft eine Funktion auf, die jeweils von Processing selbst zur Verfügung gestellt wurde.

size(200, 200);
background(0);
fill(255);
ellipse(50, 40, 20, 20);
println("fertig");

Häufiger Fehler ist, dass bei der Definition einer Funktion die Typen vergessen werden oder dass umgekehrt beim Aufruf einer Funktion der Typ mit angegeben wird.

9.3 Rückgabewert

Sie können nicht nur einer Funktion Infos mit auf den Weg geben, sondern Sie können auch eine Info (und zwar genau eine) zurückbekommen. Das ist der Rückgabewert.

Video: Funktionen 3 - Rückgabewert (9:57)

Rückgabetyp definieren

Nehmen wir an, Sie wollen eine komplexe Rechnung mit einer Funktion abhandeln, z.B. die Qudratzahl einer Zahl zu berechnen. Dann definieren Sie:

float quadratzahl(float x) {
   float erg = x * x;
   return erg;
}

Sie sehen hier zwei neue Dinge. Erstens steht da statt "void" jetzt "float". Zweitens sehen Sie das Wort "return".

Das float bedeutet, dass diese Funktion einen Wert vom Typ float zurückgibt. Wohingegen void (engl. für "nichts") bedeutet, dass eine Funktion keinen Wert zurückgibt.

Damit Sie Processing sagen können, was genau zurückgegeben werden soll (und wann), gibt es das Schlüsselwort return . Im Beispiel definieren Sie eine neue lokale Variable namens erg . Diese Variable ist nur gültig bis zur nächsten schließenden geschweiften Klammer (auf ihrer Ebene). Sobald Processing auf das Wort return trifft, wird der Wert von erg zurückgegeben und es werden keine weiteren Zeilen mehr verarbeitet.

Das heißt, jeglicher Code unterhalb von return ist sinnlos:

float quadratzahl(float x) {
   float erg = x * x;
   return erg; // Processing gibt Wert zurück und bricht ab
   println("bin ich jetzt dran?"); // wird nie ausgeführt
}

Tipp: Hinter return können auch ganze Ausdrücke stehen, d.h. in kurz wäre es so:

float quadratzahl(float x) {
   return x * x;
}

Vielleicht noch ein zweites Code-Beispiel, das etwas interessanter ist. Sie möchten testen, ob ein Punkt (px, py) innerhalb eines Rechtecks (rx, ry, rw, rh) liegt (die vier Werte definieren Eckpunkt, Breite und Höhe), z.B. um eine Kollision in einem Spiel zu gestalten. Sie definieren dazu eine Funktion, die das testet und wahr/falsch zurückgibt, also einen boolean :

boolean imRechteck(int px, int py, int rx, int ry,
                   int rw, int rh)
{
   if (px > rx && px < rx + rw && py > ry && py < ry + rh) {
      return true;
   } else {
      return false;
   }
}

Wieder der Tipp, dass Sie hinter return auch Ausdrücke stellen können. Die If-Bedingung ist nichts anderes als ein boolescher Ausdruck, der zu true oder false ausgewertet wird. Das heißt, Sie können direkt schreiben:

boolean imRechteck(int px, int py, int rx, int ry,
                   int rw, int rh)
{
   return (px > rx && px < rx + rw && py > ry && py < ry + rh);
}

Und jetzt schauen wir uns an, wie Sie die Funktionen aufrufen.

Return garantieren

Processing besteht darauf, dass bei einer Funktion mit Rückgabewert immer auf ein return trifft.

Das wird dann wichtig, wenn Sie z.B. ein if im Code haben, denn hier werden ja mitunter Code-Teile übersprungen, d.h. Ihr return wird vielleicht nie erreicht.

Schauen wir uns eine einfache Funktion an, die prüft, ob eine Zahl positiv ist:

boolean isPositive(int num) {
  if (num >= 0) {
    return true;
  }
}

Processing weigert sich, diesen Code auszuführen, denn es ist nicht garantiert, dass das return erreicht ist. Was, wenn die Zahl negtiv ist?

Den Mangel beheben wir entweder so:

boolean isPositive(int num) {
  if (num >= 0) {
    return true;
  } else {
    return false;
  }
}

Oder so:

// Alterantive Lösung ohne Else

boolean isPositive(int num) {
  if (num >= 0) {
    return true;
  }
  return false;
}

In beiden Fällen wird immer ein return erreicht.

Rückgabewerte im Code verwenden

Beim Aufruf einer Funktion mit Rückgabewert wird der Aufruf durch den Rückgabewert ersetzt. Insofern ähnelt dies dem Prinzip einer Variable, die durch ihren Inhalt ersetzt wird.

Aus

println(quadratzahl(5));

wird im nächsten Schritt also

println(25);

Rückgabewerte werden häufig zunächst an Variablen gebunden und dann weiter verarbeitet:

float z = quadratzahl(5);
println("Ergebnis: " + z);

Sie können aber den Funktionsaufruf auch direkt als Parameter für eine andere Funktion (hier wäre das println ) einbinden:

println("Ergebnis: " + quadratzahl(5));

Hier ein Beispiel mit der zweiten Funktion:

boolean collision = imRechteck(25, 40, 20, 30, 100, 100);
if (collision) {
   println("kaputt!");
}

Auch hier könnten Sie den Funktionsaufruf direkt in die If-Bedingung hineinsetzen.

if (imRechteck(25, 40, 20, 30, 100, 100)) {
   println("kaputt!");
}

Allgemein sollten Sie einen Funktionsaufruf mit Rückgabewert genauso betrachten wie eine Variable. Eine Variable vom Typ int kann überall auftreten, wo ein int -Wert stehen könnte. Genauso kann ein Funktionsaufruf mit Rückgabetyp int überall dort stehen, wo ein int -Wert stehen könnte.

Beachten die syntaktische Ähnlichkeit von Variablendeklaration und Funktionsdefinition:

float foo;

float quadratzahl(float x) {
   return x * x;
}

Beide Konstrukte, Variable und Funktion stehen stellvertretend für einen Wert von einem bestimmten Typ. Daher können beide an den gleichen Stellen stehen.

Übungsaufgaben

Denken Sie daran, Ihre Funktion immer zu testen.

(a) Einfache Funktion

Schreiben Sie eine Funktion foo, die immer den Wert 42 zurückgibt. Die Funktion hat keine Parameter und den Rückgabtyp int.

Testen Sie Ihre Funktion mit:

void setup() {
    int num = foo();
    println(num);
}

Sie sollten sehen:

42

(b) Addition

Schreiben Sie eine Funktion plus, die zwei Zahlen vom Typ float als Parameter hat und die Summe der Zahlen zurückgibt (ebenfalls Typ float).

Testen Sie Ihre Funktion mit:

void setup() {
    println(plus(10, 20));
}

Sie sollten sehen:

30.0

(c) Intervall-Check

Schreiben Sie eine Funktion bereich, die drei Zahlen a, b, c vom Typ float als Parameter hat. Die Rückgabe ist eine boolescher Wert: true, wenn a zwischen b und c liegt und false sonst.

Gehen Sie davon aus, dass b immer kleiner als c ist.

Testen Sie Ihre Funktion mit:

void setup() {
    println(bereich(10, 0, 100));
    println(bereich(-10, 0, 100));
}

Sie sollten sehen:

true
false

(d) Quadrat

Schreiben Sie die Funktion quadrat mit einem Parameter des Typs float. Die Funktion gibt die Quadratzahl des Parameters zurück.

Testen Sie Ihre Funktion mit:

void setup() {
    println(quadrat(3));
    println(quadrat(5));
}

Sie sollten sehen:

9.0
25.0

Verwenden Sie nicht die Processing-Funktion pow().

(e) Maximum

Schreiben Sie die Funktion max2, die zwei float-Werte bekommt und den größeren der beiden zurückgibt.

Schreiben Sie die Funktion max3, die drei float-Werte bekommt und den größten zurückgibt. Testen Sie unbedingt auch Fälle mit zwei gleichen Werten wie (5, 5, 1) oder (5, 1, 5).

Verwenden Sie nicht die Processing-Funktion max().

Tipp
Sie können max3 zwar mit "if" lösen, es gibt aber auch eine sehr elegante (und kurze) Lösung, wo die Funktion max2 verwendet wird. Sie bekommen dies sogar auf einer einzigen Zeile unter.

(f) Abstand

Schreiben Sie die Funktion difference, die zwei float-Werte bekommt und den Abstand zwischen den beiden Werten als postive Zahl zurückgibt.

Testen Sie Ihre Funktion mit:

void setup() {
  println(difference(5.5, 10.5));
  println(difference(1.5, 0));
}

Sie sollten sehen:

5.0
1.5

Verwenden Sie nicht die Processing-Funktion abs().

Tipp
Sie müssen unterscheiden, ob der erste Wert größer als der zweite ist - oder umgekehrt.

Zusammenfassung

Funktionen können einen Wert zurückgeben und somit ihrer Benennung als "Funktion" gerecht werden, dann auch mathematische Funktionen erzeugen einen Wert, wenn man ihnen einen Parameter übergibt.

Bei der Definition einer Funktion mit Rückgabewert muss man den Typ des Rückgabewerts angeben. Dieser Typ steht vor dem Funktionsnamen.

float quadratzahl(float x) {
  // ...
}

Im Code der Funktion muss jeder mögliche Verlauf in einer Zeile enden, die ein return ausführt. Hier wird spezifiziert, welcher Wert zurückgeliefert wird. Der Typ des Werts muss natürlich mit dem Typ übereinstimmen, der in der Kopfzeile angegeben ist.

float quadratzahl(float x) {
  float erg = x * x;
  return erg;
}

Häufiger Fehler ist, dass bei Verwendung von z.B. if vergessen wird, dass der Code verschiedene Wege nehmen kann.

float never42(float x) {
  if (x != 42) {
    return x;
  }
  // FEHLER: kein return für den Fall x == 42
}

Beim Aufruf einer Funktion mit Rückgabewert wird der Aufruf durch den Rückgabewert ersetzt ähnlich einer Variable, deren Aufruf durch ihren Wert ersetzt wird.

9.4 Funktionen und Arrays

Ein Array kann auch einer Funktion als Parameter übergeben werden. Wenn Sie z.B. die Summe eines int-Arrays berechnen wollen:

int summe(int[] zahlen) {
    int result = 0;
    for (int i = 0; i < zahlen.length; i++) {
        result += zahlen[i];
    }
    return result;
}

Wie Sie im Beispiel sehen, behandeln Sie int[] wie einen ganz normalen Datentypen.

Sie können einen Array auch als Rückgabetyp verwenden. Nehmen wir an, Sie wollen einen int-Array mit einer bestimmten Länge erzeugen, der als Voreinstellung die gleiche Zahl in allen Zellen hat:

int[] arrayMitVoreinstellung(int laenge, int zahl) {
    int[] result = new int[laenge];
    for (int i = 0; i < laenge; i++) {
        result[i] = zahl;
    }
    return result;
}

Sie behandeln auch hier den Array-Typen int[] wie einen anderen Typ. Das gleiche gilt natürlich für float-Arrays, String-Arrays etc.

Arrays sind Objekte

Besondere Vorsicht ist geboten, wenn Sie einen Array übergeben (Parameter) und den Array in der Funktion verändern. Da der Array ein Objekt ist und also nur ein Mal existiert, verändern Sie mit der Funktion diesen einen Array.

Schauen Sie sich folgenden Code an. Was passiert mit a?

void setup() {
  int[] a = {1, 2, 3};
  doSomething(a);
  println(a);
}

void doSomething(int[] arr) {
  arr[0] = 42;
}

Hier wird Array a übergeben an doSomething. Die Parametervariable arr zeigt natürlich auf den selben Array wie a. Bei diesem einen Array wird das erste Element auf 42 gesetzt. Dabei ist es egal, ob Sie über a oder arr auf diesen Array zugreifen. Folglich erhalten Sie:

[0] 42
[1] 2
[2] 3

Beachten Sie, dass das gleiche Beispiel nicht bei primitiven Datentypen funktioniert, was hoffentlich auch Ihrer Erwartung entspricht:

void setup() {
  int x = 0;
  doSomething(x);
  println(x);
}

void doSomething(int foo) {
  foo = 42;
}

Hier wird der Wert von x in die Parametervarialbe foo kopiert. Die Tatsache, dass innerhalb der Funktion das foo geändert wird, hat keinerlei Konsequenzen für x. Daher sehen Sie:

0

Natürlich ist es erlaubt, einen Array innerhalb einer Funktion zu ändern. Nur muss man sich dessen sehr bewusst sein, wenn man die Funktion aufruft. Eine Alternative ist es oft, in der Funktion einen neuen Array zu erzeugen, den übergebenen Array dort hinein zu kopieren, an der Kopie die Änderung vorzunehmen und diesen neuen Array zurückzugeben. In diesem Fall wird allerdings mehr Speicherplatz verbraucht (zwei Arrays statt einem), also muss man im Einzelfall entscheiden, was sinnvoller ist.

Übungsaufgaben

(a) Array ausgeben

Schreiben Sie eine Funktion ausgeben, die einen int-Array als Parameter bekommt und den Inhalt des Arrays in exakt folgendem Format auf der Konsole ausgibt:

Index 0 enthält 10
Index 1 enthält 12
Index 2 enthält -5

Testen Sie Ihren Code mit:

void setup() {
  int[] test = { 10, 12, -5 };
  ausgeben(test);
}

Hinweis: Achten Sie in dieser und allen folgenden Aufgaben darauf, dass Sie keine globalen Variablen innerhalb der zu definierenden Funktion verwenden.

Testen Sie die Robustheit Ihres Codes, indem Sie den Testarray um ein paar Zahlen erweitern. Ihr Code sollte weiterhin funktionieren.

(b) Durchschnitt

Schreiben Sie eine Funktion average, die einen float-Array als Parameter bekommt und den Durchschnitt der dort enthaltenen Zahlen berechnet und zurückgibt (float).

Testen Sie Ihren Code mit:

void setup() {
  float[] test = { 1.5, 3.5, 1 };
  println(average(test));
}

Testen Sie die Robustheit Ihres Codes, indem Sie den Testarray um ein paar Zahlen erweitern. Ihr Code sollte weiterhin funktionieren.

(c) Minimum

Schreiben Sie eine Funktion arrayMin, die einen float-Array als Parameter bekommt und den kleinsten Wert der dort enthaltenen Zahlen findet und zurückgibt (float).

Zum Beispiel sollten Sie für den Array { 5, -13, 32.5 } den Wert -13 zurückbekommen.

(d) Letzte negative Zahl

Schreiben Sie eine Funktion lastNegative, die einen float-Array als Parameter bekommt und den letzte negative Zahl der dort enthaltenen Zahlen zurückgibt (float).

Zum Beispiel sollten Sie für den Array { 5, -13, 32.5, -5.1, 0 } den Wert -5.1 zurückbekommen.

Tipp
In welcher Reihenfolge geht man am besten durch den Array?

(e) Array erzeugen

Schreiben Sie eine Funktion makeArray, die zwei Parameter bekommt - length und number - und einen int-Array zurückgibt. Der zurückgegebene Array soll die Länge length haben und in jeder Zelle den Wert number enthalten.

Beispiel: Mit dem Aufruf makeArray(3, 10) sollten Sie den Array {10, 10, 10} zurückbekommen.

(f) Zufallsarray erzeugen

Schreiben Sie eine Funktion createRandomArray, die einen float-Array mit Zufallszahlen (zwischen 0 und 1) enthält. Die Länge "anzahl" (int) wird als Parameter übergeben.

(g) Zahlenarray erzeugen

Schreiben Sie eine Funktion create123, die einen int-Array zurückgibt, der die Zahlen 1, 2, 3, 4, ... bis zu einer Länge "anzahl" enthält. Die Länge "anzahl" (int) wird als Parameter übergeben.

(h) Zahlenarray erzeugen II

Schreiben Sie eine Funktion numbers, die einen int-Array zurückgibt, die zwei Parameter bekommt: eine Startzahl (start) und eine Länge (len). Die Funktion soll einen Array zurückgeben, der mit der Zahl start beginnt, dann die entsprechenden aufeinander folgenden Zahlen enthält und so lang ist wie len.

Zum Beispiel bei start=5 und len=3 müsste { 5, 6, 7 } zurückgegeben werden.

Variante: Schreiben Sie außerdem die Funktion numbers2, die zwei Parameter bekommt: start und end. Sie gibt einen Array zurück, der mit der Zahl start beginnt und mit der Zahl end endet. Zum Beispiel bei start=3 und end=6 müsste { 3, 4, 5, 6 } zurückgegeben werden.

(i) Array filtern

Schreiben Sie eine Funktion filterArrayPositive, die einen int-Array numbers bekommt und einen int-Array zurückgibt. Der zurückgegebene Array enthält nur die positiven Zahlen von numbers.

Beispiel: Wenn der übergebene Array {1, -1, -20, 15} wäre, müsste der Array {1, 15} zurückgegeben werden.

Tipp
Gehen Sie in zwei Schritten vor, indem Sie zuerst herausfinden, wieviele Elemente der Ergebnisarray haben soll. Dann erzeugen Sie das Ergebnisarray und befüllen diesen (achten Sie hier auf die Indices).

(j) Klausur-Check

Schreiben Sie eine Funktion klausurCheck, die testet, ob ein Klausurteilnehmer die notwendige Punktzahl von 50 erreicht und somit bestanden hat.

Die Funktion bekommt einen Array mit Namen und einen Array mit Klausurergebnissen (Punkte). Geben Sie aus, welche Person bestanden hat.

Testen Sie Ihren Code mit:

void setup() {
  String[] namen = {"Schmidt", "Holz", "Hauser", "Eberhardt", "Drexler"};
  int[] punkte = {50, 3, 44, 98, 80};
  klausurCheck(namen, punkte);
}

Nennen Sie in Ihrer Funktion die Parameter anders als "namen" und "punkte".

Sie sollten sehen:

Schmidt hat mit 50 Punkten bestanden.
Eberhardt hat mit 98 Punkten bestanden.
Drexler hat mit 80 Punkten bestanden.

Als Zusatzaufgabe zählen Sie noch diejenigen, die durchgefallen sind und geben Sie die Zahl mit aus (letzte Zeile):

Schmidt hat mit 50 Punkten bestanden.
Eberhardt hat mit 98 Punkten bestanden.
Drexler hat mit 80 Punkten bestanden.
2 Teilnehmer sind durchgefallen.

(k) Arrayelemente tauschen

Sie möchten in einem Array zwei Elemente tauschen, also z.B. das Element mit Index 1 mit dem Element mit Index 4.

Schreiben Sie die Funktion swap, die drei Parameter hat: eine int-Array und zwei Indexzahlen (int). Die Funktion tauscht die Werte der beiden Elemente, also swap(a, 1, 4) würde bei dem Array a = {1, 2, 3, 4, 5} die Werte 2 und 5 tauschen.

Testen Sie Ihren Code mit

void setup() {
  int[] a = {1, 2, 3, 4, 5}
  swap(a, 1, 4);
  println(a);
}

Sie sollten sehen:

[0] 1
[1] 5
[2] 3
[3] 4
[4] 2

Natürlich sollte Ihre Funktion für beliebige Indexzahlen funktionieren.

(l) Array einfügen

Schreiben Sie die Funktion insertArray(), die zwei Arrays a und b und eine Zahl pos bekommt.

Die Funktion fügt Array b in Array a ein und zwar an Position pos. Sie gibt den resultierenden (neuen) Array zurück.

Testen Sie Ihren Code mit

void setup() {
  int[] a = { 1, 2, 3, 4, 5 };
  int[] b = { 101, 102, 103 };
  println(insertArray(a, b, 2));
}

Sie sollten sehen:

[0] 1
[1] 2
[2] 101
[3] 102
[4] 103
[5] 3
[6] 4
[7] 5
Tipp
Erzeugen Sie zunächst den Ergebnisarray und befüllen Sie ihn in drei Schritten (welche sind das?). Nach jedem dieser Schritte können Sie erstmal testen, ob dieser Teil funktioniert. Sie müssen etwas mit den Indices jonglieren. Vielleicht hilft eine Skizze auf Papier?

9.5 Überladen und Signatur

Processing/Java erlaubt es, mehrere Funktionen mit dem selben Namen zu definieren. Diese Eigenschaft einer Programmiersprache nennt man auch Überladen (overloading), weil man quasi einen einzigen Namen mit mehreren Funktionen belädt.

Hier haben wir z.B. zwei verschiedene Funktionen, die beide foo heißen:

// Beispiel 1: Zwei Funktionen, gleicher Name

void setup() {
  foo(1);
  foo(1, 2);
}

void foo(int a) {
  println(a);
}

void foo(int x, int y) {
  println(x + y);
}
1
3

Woher weiß Processing, welche Funktion jeweils genommen werden soll? In dem Beispiel könnte man meinen, dass Processing auf die Anzahl der Parameter schaut. Das ist richtig, ist aber nur die halbe Wahrheit. Man kann nämlich auch zwei Funktionen mit der gleichen Anzahl von Parametern definieren:

// Beispiel 2: Gleiche Anzahl von Parametern

void setup() {
  foo(1, 2);
  foo(true, false);
}

void foo(int x, int y) {
  println("Addition: " + (x + y));
}

void foo(boolean x, boolean y) {
  println("Die Wahrheit ist: " + (x && y));
}

Als Ausgabe bekommen wir:

Addition: 3
Die Wahrheit ist: false

Das heißt, auch hier wählt Processing "die richtigen" Funktionen aus, indem Processing auf die Datentypen der Parameter schaut (int im ersten Fall, boolean im zweiten Fall).

Wie sieht es mit folgendem Beispiel aus?

// Beispiel 3: Gleiche Anzahl und Typen der Parameter

void setup() {
  foo(42, "hallo");
  foo("hi", 18);
}

void foo(int x, String s) {
  println(s + ":" + x);
}

void foo(String s, int x) {
  println(s + "#" + x);
}

Wir haben zwei Funktionen, die beide gleich heißen, die gleiche Anzahl von Parametern haben und die gleichen Typen. Und doch funktioniert der Code:

hallo:42
hi#18

Der Grund ist: die Reihenfolge der Parametertypen ist unterschiedlich. Das erste foo hat erst int, dann String. Das zweite foo hat erst String, dann int.

Signatur

Allgemein sagt man, dass Processing die Signatur einer Funktion zu Rate zieht. Die Signatur ist definiert über:

  • den Namen der Funktion
  • die Anzahl, Reihenfolge und Typen der Parameter

Im den oberen Beispielen hätte man folgende Signaturen.

Beispiel 1:

foo(int)
foo(int, int)

Beispiel 2:

foo(int, int)
foo(boolean, boolean)

Beispiel 3:

foo(int, String)
foo(String, int)

Sie bemerken vielleicht zwei Dinge: (1) alle Signaturen sind unterschiedlich und (2) die Variablennamen tauchen gar nicht auf, sind also irrelevant.

Wenn man die jeweilige Signatur einer Funktion hinschreibt, sieht man also sofort, ob man die jeweilige Funktion definieren darf oder nicht.

Die Regel lautet: Man darf nur Funktionen definieren, die unterschiedliche Signaturen haben.

Dies Beispiel ist demnach auch korrekt:

// Überladen von foo mit jeweils einem Parameter

void setup() {
  foo(true);
  foo(32);
}

// Signatur ist foo(int)

void foo(int x) {
  println(10 + x);
}

// Signatur ist foo(boolean)

void foo(boolean b) {
  println("Wahrheitswert: " + b);
}
Wahrheitswert: true
42

Wie gesagt spielt es keine Rolle, wie Ihre Parameter heißen, also ob Sie schreiben foo(boolean b) oder foo(boolean blabla) . Für die Signatur ist nur der Typ des jeweiligen Parameters relevant.

Beachten Sie, dass der Rückgabetyp nicht zur Signatur gehört. Dass bedeutet, Sie dürfen nicht zwei Funktionen mit gleicher Signatur, aber unterschiedlichem Rückgabetyp definieren:

// So geht das nicht!

void setup() {
  foo(true, false); // hier wird Fehler gemeldet
}

// Signatur ist foo(boolean, boolean)
void foo(boolean x, boolean y) {
  println("Die Wahrheit ist: " + (x && y));
}

// Signatur ist auch foo(boolean, boolean)
// Daher der Fehler!
boolean foo(boolean x, boolean y) {
  return x && y;
}

Zusammenfassung

  • Man kann mehrere Funktionen mit dem selben Namen definieren. Das nennt man Überladen.
  • Die Signatur einer Funktion besteht aus dem Namen und der Liste der Parametertypen (wobei die Reihenfolge wesentlich ist)
  • Man darf nur Funktionen definieren, die unterschiedliche Signaturen haben

9.6 Skopus von Variablen

Sie kennen bereits den Unterschied zwischen globalen Variablen (oben im Code deklariert) und lokalen Variablen (innerhalb eines Code-Blocks deklariert). Hier sehen wir uns den Gültigkeitsbereich (Skopus) von Variablen etwas genauer an.

Lokale Variablen sind immer nur im eigenen Code-Block gültig, ab der Zeile, in der sie deklariert werden. Im folgenden sehen wir die verschiedenen Gültigkeitsbereiche von einer globalen Variablen und zwei lokalen Variablen innerhalb ihrer jeweiligen Code-Blöcke. Der "Code-Block" der globalen Variable x ist das gesamte Programm!

Den Gültigkeitsbereich einer Variablen nennt man auch den Skopus der Variablen (engl. scope).

Hier nochmal der Code zum Ausprobieren:

// Bring die Maus zwischen Ball und linker Wand,
// dann ändert sich die Farbe

int x = 0;

void draw() {
  background(255);
  checkBounds();
  ellipse(x, height/2, 20, 20);
  x++;
}

void checkBounds() {
  int middle = x / 2;

  if (mouseX > middle - 5 && mouseX < middle + 5) {
    float ran = random(1);
    fill(ran * 255);
  }

  if (x > width) {
    x = 0;
  }
}

Übungsaufgaben

(a) Finde den Skopus

In welchen Bereichen gelten die Variablen x, num und s? Überlegen Sie zuerst und merken Sie sich Ihre Entscheidung. Überprüfen Sie dann Ihre Überlegen durch einkommentieren der entsprechenden Print-Anweisungen.

int x = 0;

void setup() {
  int num = 20;
  size(200, 200);
  // println(num);
  // println(s);
  // println(x);
}

void draw() {
  ellipse(x, 50, 20, 20);
  if (x > width) {
    String s = "hello";
    x = 0;
    // println(num);
    // println(s);
    // println(x);
  }
  // println(num);
  // println(s);
  // println(x);
}