Lernziele
- Sie verstehen die Bestandteile und den Aufbau einer Funktion, inklusive Parameter und Rückgabetyp/-wert
- Sie können eigene Funktionen definieren
- Sie können Funktionen in verschiedenen Kontexten aufrufen
- Sie können Arrays als Parameter und als Rückgabewert von Funktionen einsetzen
- Sie verstehen, in welchen Situationen eine neue Funktion sinnvoll ist, um den Code übersichtlicher zu gestalten oder um Codeteile wiederverwendbar zu machen
- Sie können mehrere Funktionen mit gleichem Namen definieren (Überladen) und per Signatur unterscheiden
- Sie verstehen die Reichweite (Skopus) von Variablen und können den Skopus von konkreten Variblen erklären
Voraussetzungen
Sie sollten sicher mit Variablen umgehen können Kapitel 2.
Sie sollten sicher mit If-Anweisungen umgehen können Kapitel 3.
Ab Abschnitt 7.4 sollten Sie auch sicher mit Schleifen Kapitel 5 sowie mit Arrays Kapitel 6 und Objekten Kapitel 4 umgehen können.
- 23.01.2024: Level zu Aufgaben hinzugefügt
- 02.10.2021: Lernziele angepasst
- 02.08.2021: Neue Kapitelnummerierung
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.
7.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.
Fingerübungen
a) Hello, world!
Schreiben Sie die Funktion hello
, die einfach "hello, world" auf die Konsole schreibt.
Da man für die Nutzung von Funktionen im aktiven Modus sein muss, benötigen Sie ein setup
oder draw
. Wenn wir keine Animation/Interaktion brauchen, reicht ein setup
.
Testen Sie Ihren Code mit:
void setup() { hello(); hello(); hello(); }
Sie sollten sehen:
hello, world hello, world hello, world
void hello() { println("hello, world"); }
Übungsaufgaben
7.1 a) Mauszeiger Level 11 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
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(); }
7.1 b) Code durch Funktion ersetzen Level 11 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
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); }
7.1 c) Dreierlei Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
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.
void setup() { one(); }
Sie sollten sehen:
1 2 3
Wenn Sie Ihren Code wie folgt testen wollen, haben Sie etwas 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.
7.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
printMittelwert
gültig.
void setup() { printMittelwert(5.0, 12.5); } void printMittelwert(float a, float b) { float summe = a + b; println(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() { printMittelwert(5.0, 12.5); println(a); // Fehler! Variable a ist hier nicht gültig. } void printMittelwert(float a, float b) { float summe = a + b; println(summe / 2); }
Parameter nicht verändern
Innerhalb einer Funktion gilt für die Parameter:
Hier ein Beipiel, was gemeint ist:
// Schlechter Stil! void printSumme(int a, int b) { a = a + b; // Parameter a wird verändert println(a); }
Der obere Code führt zwar nicht zu einem Fehler, zeigt aber schlechten Stil,
da bei längerem Code unklar ist, wann der Parameter a
den
ursprünglichen Wert hat und wann einen veränderten Wert.
Im Beispiel können Sie stattdessen eine neue lokale Variable einführen:
// Stattdessen lokale Variable: void printSumme(int a, int b) { int c = a + b; println(c); }
In diesem Beispiel können Sie natürlich auch viel einfacher dies schreiben:
void printSumme(int a, int b) { println(a + b); }
Diese Regel gilt nur für primitive Datentypen (int, float, boolean ...). Bei Objekten (z.B. PVector) und Arrays (sind ebenfalls Objekte) kann es sein, dass man will, dass ein Parameter und damit das referenzierte Objekt verändert wird.
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. Dies hängt mit der Signatur der Funktion zusammen (siehe entsprechenden Abschnitt unten).
Schauen wir uns nochmal die zwei Funktionen von oben an. Zunächst nochmal die Definitionen:
void zeichneGesicht(int augenfarbe) { fill(255); ellipse(x, y, 80, 80); fill(augenfarbe); ellipse(x-15, y-15, 20, 20); ellipse(x+15, y-15, 20, 20); } void zeichneGesicht2(int gesichtsfarbe, int augenfarbe) { fill(gesichtsfarbe); ellipse(x, y, 80, 80); fill(augenfarbe); ellipse(x-15, y-15, 20, 20); ellipse(x+15, y-15, 20, 20); }
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); zeichneGesicht2(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); zeichneGesicht2(farbe1, farbe2);
Fingerübungen
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
void plusEins(int x) { println(x + 1); }
Übungsaufgaben
7.2 a) Summe ausgeben Level 11 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
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
7.2 b) Sterne sehen Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
Schreiben Sie eine Funktion sternchen
, die als Parameter eine ganze Zahl n
bekommt und dann n
Sternchen auf der Konsole ausgibt.
Der Aufruf sternchen(5)
würde folgendes ausgeben:
*****
Hinweis: Verwenden Sie eine Schleife.
7.2 c) Mauszeiger mit Parametern Level 11 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
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.
7.2 d) Begrüßung Level 11 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
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
7.2 e) Begrüßung mit Gender Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
Schreiben Sie eine neue Funktion greet
, diesmal mit zwei Parametern, einen String und einen boolean. Der zweite Parameter gibt an, ob die Person weiblich ist.
Beim Aufruf
greet("Schmidt", true);
soll kommen:
Hallo Frau Schmidt
Bei
greet("Huber", false);
soll kommen:
Hallo Herr Huber
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.
7.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
return
erreicht wird.
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.
Fingerübungen
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); }
Auch dies sollte funktionieren:
void setup() { println(foo()); }
Sie sollten sehen:
42
int foo() { return 42; }
b) Addieren
Schreiben Sie eine Funktion plus, die zwei Zahlen vom Typ float als Parameter hat und die Summe der Zahlen (ebenfalls float) zurückgibt.
Testen Sie Ihre Funktion mit:
void setup() { println(plus(10, 20)); println(plus(5, -3)); }
30.0 2.0
float plus(float a, float b) { float c = a + b; return c; }
Es geht auch kürzer:
float plus(float a, float b) { return a + b; }
Übungsaufgaben
7.3 a) Intervall-Check Level 11 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
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
7.3 b) Quadrat Level 11 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
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
.
7.3 c) Maximum Level 31 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
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
.
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.
7.3 d) Abstand Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
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
.
7.3 e) Gerade-ungerade Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
Schreiben Sie die Funktion istGerade
, die eine ganze Zahl bekommt und einen Wahrheitswert zurückgibt,
nämlich true, wenn die Zahl gerade ist, false wenn ungerade.
7.3 f) Primzahl Level 41 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
Schreiben Sie die Funktion istPrim
, die eine ganze Zahl bekommt und einen Wahrheitswert zurückgibt,
nämlich true, wenn die Zahl eine Primzahl ist, false sonst.
Eine Primzahl ist eine Zahl, die nur durch 1 und durch sich selbst teilbar ist. Zum Beispiel ist die 7 eine Primzahl, da sie nur durch 1 und 7 teilbar ist. Die 8 ist keine Primzahl, da sie durch 1, 2, 4 und 8 teilbar ist.
Testen Sie Ihren Code mit:
void setup() { println(istPrim(7)); println(istPrim(15)); println(istPrim(97)); }
true false true
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.
7.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.
Fingerübungen
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 101 Index 1 enthält 120 Index 2 enthält -500
Testen Sie Ihren Code mit:
void setup() { int[] test1 = { 1, 2, 3 }; int[] test2 = { 101, 120, -500 }; ausgeben(test1); ausgeben(test2); }
void ausgeben(int[] a) { for (int i = 0; i < a.length; i++) { println("Index " + i + " enthält " + a[i]); } }
b) Array erzeugen
Schreiben Sie eine Funktion makeIntArray
,
die eine ganze Zahl n als Parameter bekommt und
einen neuen Integer-Array der Länge n zurückgibt.
Testen Sie Ihren Code mit:
void setup() { println(makeIntArray(5)); }
[0] 0 [1] 0 [2] 0 [3] 0 [4] 0
int[] makeIntArray(int n) { int[] a = new int[n]; return a; }
Man kann es auch ganz kompakt so schreiben:
int[] makeIntArray(int n) { return new int[n]; }
Übungsaufgaben
Achten Sie in allen folgenden Aufgaben darauf, dass Sie keine globalen Variablen innerhalb der zu definierenden Funktion verwenden.
7.4 a) Durchschnitt Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
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.
7.4 b) Minimum Level 31 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
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.
7.4 c) Erste/letzte negative Zahl Level 31 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
Schreiben Sie zwei Funktionen.
Die erste Funktion firstNegative
bekommt
einen float-Array als Parameter und gibt
die erste negative Zahl der dort enthaltenen Zahlen zurück.
Zum Beispiel sollten Sie für den Array { 5, -13, 32.5, -5.1, 0 } den Wert -13 zurückbekommen.
Die zweite Funktion lastNegative
bekommt auch
einen float-Array als Parameter und gibt
die letzte negative Zahl der dort enthaltenen Zahlen zurück.
Zum Beispiel sollten Sie für den Array { 5, -13, 32.5, -5.1, 0 } den Wert -5.1 zurückbekommen.
lastNegative
?
Testen Sie Ihre Funktionen mit:
void setup() { float[] test = { 5, -13, 32.5, -5.1, 0 }; println(firstNegative(test)); println(lastNegative(test)); }
7.4 d) Array erzeugen Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
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.
7.4 e) Zufallsarray Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
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.
7.4 f) Zahlenarray erzeugen Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
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.
7.4 g) Zahlenarray mit Start und Länge Level 31 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
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.
7.4 h) Array filtern Level 31 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
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.
7.4 i) Klausur-Check Level 31 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
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 zwei Parameter anders als namen
und punkte
. Das ist zwar nicht zwingend notwendig, aber es ist konzeptionell verwirrend, wenn die Variablen und Parameter gleich heißen.
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.
7.4 j) Arrayelemente tauschen Level 31 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
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.
7.4 k) Array einfügen Level 41 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
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
7.4 l) Negative Werte entfernen Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
Schreiben Sie die Funktion noNegatives
, die eine int-Array bekommt und in dem Array alle negativen Zahlen durch Null ersetzt.
Testen Sie Ihren Code mit:
void setup() { int[] a = { 1, 5, -10, 20, -3 }; noNegatives(a); println(a); }
[0] 1 [1] 5 [2] 0 [3] 20 [4] 0
7.4 m) One-Hot-Encoding als Funktion Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
Im Bereich des Deep Learning muss man Zahlen wie 2 oder 4 mit jeweils einen Vektor darstellen. Man wandelt diese Zahlen dann in Vektoren um (z.B. mit Länge 5), die überall eine Null haben außer an der entsprechenden Stelle, wo eine 1 steht. Die Zahl 2 wird also zum Vektor (0 0 1 0 0) und die Zahl 4 zu (0 0 0 0 1).
Schreiben Sie eine Funktion onehot
, die zwei Zahlen z
und len
bekommt. len
ist die Länge des Vektors (Arrays) und z
die Zahl die enkodiert werden soll. Die Funktion soll einen Array entsprechend der Kodierung zurückgeben.
7.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.
Signatur
Die Signatur einer Funktion besteht aus den folgenden Informationen:
- Name der Funktion
- Anzahl, Reihenfolge und Typen aller Parameter
Nicht zur Signatur gehören die Namen der Parameter. Auch der Rückgabewert gehört nicht zur Signatur.
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 der Parameter 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; }
Übungsaufgaben
7.5 a) Begrüßung mit Varianten Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
Schreiben Sie mehrere Varianten der Funktion greet
, so dass folgender Code funktioniert:
void setup() { greet("Schmidt"); greet("Schmdit", true); greet("Schmidt", false, "Dr."); }
Der boolesche Parameter beim zweiten und dritten Aufruf bezieht sich auf das Geschlecht der Person (true = weiblich).
Die Ausgabe sollte wie folgt aussehen:
Guten Tag, Frau oder Herr Schmidt Guten Tag, Frau Schmdit Guten Tag, Herr Dr. Schmidt
7.5 b) Summe Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
Schreiben Sie zwei Varianten der Funktion add
, so dass sowohl int-Zahlen
als auch Vektoren addiert werden können. Geben Sie die entsprechende Zahl bzw. den Vektor zurück.
Achten Sie bei der Vektoraddition darauf, dass die übergebenen Vektoren nicht verändert werden.
Testen Sie Ihr Programm mit:
void setup() { int summe = add(10, -5); PVector v1 = new PVector(1, 1); PVector v2 = new PVector(100, 50); PVector summeVek = add(v1, v2); println("Summe Zahlen = " + summe); println("Vektor 1 = " + v1); println("Vector 2 = " + v2); println("Summe Vektoren = " + summeVek); }
Sie sollten sehen:
Summe Zahlen = 5 Vektor 1 = [ 1.0, 1.0, 0.0 ] Vector 2 = [ 100.0, 50.0, 0.0 ] Summe Vektoren = [ 101.0, 51.0, 0.0 ]
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
7.6 Skopus von Variablen
Sie kennen bereits den Begriff Skopus aus Kapitel 2.5, wo wir den Unterschied zwischen globalen Variablen (oben im Code deklariert) und lokalen Variablen (innerhalb eines Code-Blocks deklariert) besprochen haben. Hier sehen wir uns den Skopus nochmal am Beispiel einer Funktion 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 Skopus der globalen Variable x
ist das gesamte Programm, inklusive der Funktion checkBounds
.
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
7.6 a) Finde den Skopus Level 21 = easy
2 = relativ leicht
3 = mittel
4 = schwierig
5 = hart
In welchen Bereichen gelten die Variablen x
, num
und s
? Überlegen Sie zuerst und merken Sie sich Ihre Entscheidung. Überprüfen Sie dann Ihre Überlegung 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); }