Funktion
Motivation
Eine Grundregel der Programmierung lautet: So wenig komplex wie möglich, teile die zu lösende Aufgabe in Teilschritte, so dass jeder Teilschritt übersichtlich wird! Eine Möglichkeit zur Zerlegung einer Aufgabe bietet eine Funktion.
Weitere Vorteile:
Wiederverwendbarkeit: Ist ein Teilproblem mal gelöst, kann es oft in anderem Zusammenhang wieder benutzt werden. Das spart Zeit.
Lesbarkeit: wird der Name der Funktion gut gewählt, sieht man auf den ersten Blick, was ein Programm leistet:
Beispiel: Berechnung des Kugelvolumens der Erde
- Hinweis: Die Formel für das Kugelvolumen mit Radius r lautet: v = 4/3*pi*r³
- Hinweis: Der durchschnittliche Erdradius beträgt: 6371 km
- schlecht lesbar, ohne Funktion: v = 4 / 3 * 3.14 * 6371 * 6371 * 6371
- gut lesbar, mit Funktion:
double kugelvolumen(double r) => 4 / 3 * 3.14 * r * r * r; volumenErde = kugelVolumen(6371);
Obwohl die zweite Variante mehr Platz braucht, ist sie auf einen Blick erfassbar, die erste dagegen eher nicht.
Beispiel zur Problemzerlegung
Es soll ein Programm erstellt werden, das die Reichweite von Elektoautos für bestimmte Strecken errechnet.
Physikalische Grundlagen:
- Fahrwiderstand entzieht Energie. Diese ist näherungsweise nur von der zurückgelegten Strecke und der Geschwindigkeit abhängig.
- Bergauffahrten entziehen Energie: Diese ist näherungsweise nur von der Summe der Höhenmeter der Bergaufstrecken abhängig.
- Bergabfahrten gewinnen Energie (Rekuperation): Diese ist näherungsweise von der Summe der Höhenmeter der Bergabfahrten und einem Effizienzfaktor abhängig.
Aufgabe: Wie viel Energie braucht ein Auto von München nach Venedig?
Vorgehensweise:
- Gegeben ist die Masse des Autos: 1500 kg, die Durchschnittsgeschwindigkeit 100 km/h.
- Erstellung einer Funktion, die die Energie des Fahrwiderstandes in der Ebene in Abhängigkeit von der Entfernung berechnet. Physikalische Grundlagen Musterlösung Fahrwiderstand
- Erstellung einer Funktion, die die Energie einer Bergauffahrt in Abhängigkeit von den Höhenmetern und der Masse berechnet. Musterlösung Bergauf
- Erstellung einer Funktion, die die Energie einer Bergabfahrt in Abhängigkeit von den Höhenmetern und der Masse berechnet. Musterlösung Bergab
- Ermitteln der Teilstrecken von München nach Venedig, jeweils mit Entfernung, Höhenmetern bergauf und bergab. Musterlösung Teilstrecken
- Anwenden der drei obigen Funktionen für jede Teilstrecke. Musterlösung Reise
Anmerkung: Die Musterlösungen können erst verstanden werden, nachdem der Rest des Kapitels gelesen und verstanden ist. Das Beispiel oben soll nur aufzeigen, wie ein komplexes Problem mittels Funktionen in kleine, verständliche Portionen aufgeteilt wird, die dann mehr oder weniger leicht zu lösen sind.
Minifunktionen
Minifunktionen enthalten im Funktionsrumpf nur genau eine Formel.
Syntax
ergebnistyp name ( parameter-liste ) => ergebnis-formel ;
- Die Parameterliste ist entweder leer oder eine Aufzählung von Parametern in Form von: parameter-typ parameter-name.
Namenskonvention
- Der Name einer Funktion beginnt mit einem Kleinbuchstaben.
- Wenn möglich, verwende als Name ein Verb: beispielsweise "berechne()" statt "formel()". Zusammen mit der Namenskonvention für Variable (dort haben wir uns auf Substantive festgelegt) verhindert dies Überschneidungen.
- Setzt sich der Name aus mehreren Teilen zusammen, ist, wie bereits erwähnt, die "Kamelschreibweise" (camel case) üblich: Die Anfangsbuchstaben von Wörtern werden groß geschrieben: holeKonfiguration(), getService().
Aufgabe
Gib eine Liste aus, in der Durchmesser, Oberfläche und Volumen einer Kugel für 0.1, 1, 10, 100 und 1000 mm gegenübergestellt werden.
Musterlösung
void main() { const pi = 3.14159265; double oberflaeche(double r) => 4 * pi * r*r; double volumen(double r) => 4 / 3 * pi * r*r*r; print("Durchmesser: Fläche: Volumen:"); for (var durchmesser=0.1; durchmesser<=1000; durchmesser*=10){ final flaeche = oberflaeche(durchmesser/2); final vol = volumen(durchmesser/2); print("$durchmesser $flaeche $vol"); } }
- Zuerst die Definition von zwei Minifunktionen, jeweils mit Ergebnistyp double (Gleitpunktzahl) und einem Parameter "radius" mit Typ double.
- Ausgabe der Überschrift der Tabelle:
print("Durchmesser: Fläche: Volumen:");
- Erzeugen der benötigten Durchmesser mit einer gezählten Schleife. Besonderheit: die Fortschaltung ist hier eine Multiplikation mit 10:
durchmesser *= 10
. Das ist die Kurzschreibweise fürdurchmesser = durchmesser * 1000
- Berechnen der Zwischenergebnisse
flaeche
undvol
: Die Variablen dürfen nicht die gleichen Namen wie die Funktionen haben, ansonsten käme es zu einer Fehlermeldung. Denn jeder Name darf nur einmal vergeben werden (für Fortgeschrittene: den "Namensraum" vergessen wir hier mal, das kommt später).
Kleiner Ausflug: Interpretation der berechneten Tabelle
- Frage: Warum sind kleine Tiere relativ viel robuster als große Tiere? Wird eine Ameise vom Hochhaus geworfen, passiert ihr gar nichts. Einem Hund dagegen schon (bitte als reines Gedankenexperiment!).
- Die nachfolgenden Überlegungen liefern die Lösung, wenn man folgendes zugrunde legt (es geht hierbei lediglich um Größenordnungen, nicht um genaue Zahlen):
- Der Sturzschaden ist linear abhängig vom Gewicht: Doppeltes Gewicht, doppelte Zerstörungskraft an den Knochen.
- Das Gewicht ist linear abhängig vom Volumen: ist das Volumen doppelt so groß, verdoppelt sich auch das Gewicht.
- Die Robustheit ist linear abhängig vom Querschnitt der "Bauteile": verdoppelt sich die Fläche, verdoppelt sich die Robustheit.
- Vergleicht man eine Ameise (Länge: 1 mm) mit einem Hund (Länge: 1m = 1000 mm):
- Größenfaktor (Verhältnis der Durchmesser): 1000
- Gewichtsfaktor: (Verhältnis der Volumina): 1000*1000*1000 = 1 Milliarde
- Robustheitsfaktor: (Verhältnis der Flächen): 1000*1000 = 1 Million
- Verhältnis Robustheit zu Gewicht: 1 Million / 1 Millarde = 1/1000. Eine Ameise ist also 1000 mal robuster als ein Hund. Eine Ameise kann das 600-fache ihres Eigengewichts tragen, ein Hund nicht!
Aufgaben Zinseszinsrechnung
- Gib für die Jahre 1 bis 10 aus, wie sich das Kapital entwickelt, wenn der Zinssatz 3 % beträgt. Zinsformel für ein Jahr:
kapital2 = kapital*(1 + zinssatz/100.0)
Musterlösung Zinseszins - Wie lange braucht es, bis sich das Jahr bei 3 % verdoppelt? Musterlösung Kapitalverdoppelung
Allgemeine Funktionen
Im Gegensatz zur Minifunktion hat die allgemeine Funktion einen Anweisungsblock mit beliebig vielen Anweisungen.
Für allgemeine Funktionen gibt es den zusätzlichen Ergebnistyp void: das besagt, dass die Funktion kein Ergebnis hat. void ist ein besonderer Typ, der nur als Funktionsergebnis vorkommen kann.
Syntax
ergebnistyp name ( parameter-liste ) anweisungs-block'
- Die Parameterliste ist entweder leer oder eine Aufzählung von Parametern, getrennt mit Komma: parameter-typ parameter-name.
Return-Anweisung
Eine Funktion kann einen Wert zurückgeben. Welchen Wert, das wird mit der return-Anweisung definiert, indem nach dem Schlüsselwort eine Formel folgt. Gleichzeitig wird die Funktion dann an dieser Stelle verlassen. Steht dahinter noch weiterer Code, wird dieser nicht abgearbeitet. Der Compiler merkt das und gibt eine Warnung aus.
Beispiel:
int quadrat(int zahl) { return zahl * zahl; }
Ist der Funktionsergebnistyp void
, steht keine Formel nach dem return. Eine solche Funktion braucht am Ende kein return
, das wird automatisch ergänzt.
Beispiel: <pre>void gewinn(int zahl){ if (zahl < 6){ return; } print("Du hast gewonnen"); }
Beispiel 1: die main-Funktion
Die main-Funktion haben wir ja schon kennengelernt: sie muss in jedem Dart-Programm vorhanden sein.
void main() { print('Hi'); }
- Der Ergebnistyp der main-Funktion ist void: das besagt, dass die Funktion kein Ergebnis hat.
- void ist ein besonderer Typ, der nur als Funktionsergebnis vorkommen kann.
- Die Parameterliste ist leer.
Beispiel 2: Passwortgenerator
Wir generieren ein gut lesbares bzw. merkbares Passwort: Es soll zufällige Buchstaben enthalten, aber abwechselnd Vokale und Konsonanten, damit das Ergebnis lesbar wird. Am Ende soll noch eine 2-stellige Zahl stehen. Vorkommen sollen sowohl Groß- als auch Kleinbuchstaben, der Einfachheit halber die Vokale klein geschrieben, die Konsonanten groß.
int _seed = 584028490; int zufall() { _seed = _seed * 47121 + 392483921; return _seed; } String vokal(){ final auswahl = zufall(); switch(auswahl % 6){ case 0: return 'a'; case 1: return 'e'; case 2: return 'i'; case 3: return 'o'; case 4: return 'u'; case 5: return 'y'; } return ''; } String konsonant(){ final auswahl = zufall(); switch(auswahl % 7){ case 0: return 'P'; case 1: return 'M'; case 2: return 'K'; case 3: return 'L'; case 4: return 'R'; case 5: return 'S'; case 6: return 'X'; } return ''; } String password(int length){ String rc = ''; length -= 2; while(length > 0){ if (length % 2 == 0){ rc += vokal(); } else { rc += konsonant(); } length--; } return rc + (zufall() % 100).toString(); } void main() { print(password(8)); }
Einschub: Hier wird zum ersten mal ein Gleichheitsvergleich angestellt: length % 2 == 0
:
Es wird verglichen, ob die Werte length % 2
gleich 0 ist. Das Zeichen für Gleichheit ist '==' und nicht '=', da '=' schon für Zuweisungen vergeben ist. Näheres siehe Formel.
- Wir brauchen einen (Pseudo-)Zufallsgenerator: Dazu definieren wir die Variable
int _seed = 584028490;
. - Diese Zahl wird in der Funktion
zufall()
geändert und deren Wert zurückgeliefert. Das Ergebnis ist eine nicht einfach vorhersehbare Zahl, was uns als "Zufall" vorkommt. - In der Funktion
vokal()
wird aus der Zufallszahl genau ein Vokal generiert, wobei 'y' auch als Vokal gilt, da "sprechend". - Besonderheit in
vokal()
: in der Fallunterscheidung braucht es kein "break", weil dafür einreturn
steht. - In der Funktion
konsonant()
entsteht so ein zufälliger Konsonant. - In der Funktion password() werden abwechselnd Vokale und Konsonanten mit den obigen Funktionen generiert und an das Passwort angehängt.
- Hier wird ausgenutzt, dass mit '+' zwei Strings zusammengefügt werden können.
String rc = 'a'; rc += 'b';
sorgt dafür, dass rc den Wert 'ab' hat.rc += 'b'
ist die Abkürzung fürrc = rc + 'b'
. - Am Ende von
password()
wird an das bisherige Passwort noch eine Zahl angehängt: Da an einen String nur ein String angehängt werden kann, müssen wir die Zahl(zufall() % 100)
in eine String verwandeln. Das erledigt das angehängte.toString()
.
Hinweise:
- Das hier verwendete Konzept einer globalen Variablen (_seed) ist schlechter Programmierstil. Solange wir jedoch noch keine "Klassen" eingeführt haben, haben wir keine andere Möglichkeit.
- Das Programm liefert immer das gleiche Passwort. Ändere den Wert für
_seed
in der ersten Zeile, dann wird ein anderes generiert.
Optionale Parameter
Es kann sinnvoll sein, dass ein Parameter einen Standardwert hat und nur in Ausnahmefällen variiert wird. Dann kann man den Parameter optional machen, mit sinnvoller Vorbelegung.
Funktionsdefinition: Optionale Parameter stehen immer am Ende der Parameterliste und werden zwischen eckigen Klammern definiert.
Funktionsaufruf: Optionale Parameter brauchen nicht angegeben werden, allerdings kann dies "von hinten her" notwendig werden: Wird ein optionaler Parameter beim Aufruf angegeben, müssen alle vorher definierten optionalen Parameter auch angegeben werden.
void drucke(String ort, String postleitzahl, [String land = 'D']){ print("$land-$postleitzahl $ort"); } void main(){ drucke('Berlin', '10101'); drucke('München', '80133'); drucke('Wien', '1012', 'A'); }
- Der Parameter
land
ist optional, da er innerhalb der eckigen Klammern steht. Er ist mit dem String 'D' vorbelegt. - Für die Mehrzahl der Aufrufe gilt die Voreinstellung 'D', daher wird der Parameter nicht angegeben.
Benannte Parameter
Es kommt vor, dass eine Funktion viele Parameter hat, die beim Aufruf nicht unterscheidbar sind. Dann sind benannte Parameter eine große Hilfe. Das wird vor allem bei GUI-Klassen von Flutter äußerst hilfreich, da dort durchaus 100 Parameter und mehr auftreten können.
Funktionsdefinition': Benannte Parameter stehen am Ende der Parameterliste und werden mit '{' und '}' geklammert.
Funktionsaufruf:: Hier wird vor jedem benannten Parameter der Name des Parameters angegeben. Die Reihenfolge ist nicht wichtig.
void drucke(String ort, { String postleitzahl, String land = 'D'}){ print("$land-$postleitzahl $ort"); } void main(){ drucke('Berlin', postleitzahl: '10101'); drucke('München', postleitzahl: '80133'); drucke('Wien', land: 'A', postleitzahl: '1012'); }
- Der erste Parameter
ort
ist ein normaler Parameter, muss also an erster Stelle angegeben werden. - Der Quellcode wird durch benannte Parameter wesentlich besser lesbar.
- Benannte Parameter können beim Aufruf auch weggelassen werden, sind also automatisch optional.
- Die Reihenfolge im Aufruf ist egal.
Mischen von optionalen und benannten Parametern
Ein Mischen der Parametern ist nicht möglich: Entweder man benutzt optionale oder benannte Parameter in einer Funktion, beides gleichzeitig ist nicht möglich!
Weiter geht es mit Kapitel Formel.