Swift-Tutorial Teil 2: Diesmal ohne mogeln

Im ersten Blog haben wir gelernt, wie man das Ergebnis eines mathematischen Ausdrucks mithilfe von Swift berechnen kann. Allerdings haben wir ein bisschen gemogelt. Wir erinnern uns: Die Anweisung print(“1 + 1 = \(1 + 1)”) liefert das korrekte Ergebnis 2. Das tut sie aber nur, weil wir selbst angeben, was genau berechnet werden soll, nämlich mit der Anweisung \(1 + 1). Um einen echten Taschenrechner zu programmieren, müssen wir aber herausbekommen, wie wir aus einer Zeichenkette, wie beispielsweise “1 + 1”, das Ergebnis 2 berechnen, ohne, dass wir dem Programm vorkauen, was es genau berechnen soll – das soll das Programm selbständig erkennen.


Um diese Anforderung zu erfüllen, müssen wir zwei Probleme klären:

  1. Wie kann ein Programm aus einer Zeichenkette erkennen, was es berechnen soll? Anders gesagt: Wenn ich die Zeichenkette “1 + 1” habe, wie erkennt das Programm, dass die “1” eine numerische 1 ist und das “+” Addition bedeutet?
  2. Wie kann ich eine mathematische Operation allgemein halten? Das heißt, ich möchte in der Lage, sein a + b zu berechnen, wobei a und b jeweils irgendeine Zahl sind, und nicht nur 1 + 1.

In diesem Blogartikel werden wir uns mit Punkt 2 beschäftigen und verschieben Punkt 1 auf den nächsten Artikel.

Punkt 2 wiederum führt uns direkt zum Thema Funktionen. 

Rakete-Smartphone-App-Swift


Funktionen und Standardtypen

In Swift ist eine Funktion ein Stück Sourcecode, der eine spezifische Aufgabe erfüllt. So können wir uns eine Funktion ausdenken, die in der Lage ist, zwei beliebige Zahlen miteinander zu addieren. Um besser zu verstehen, was eine Funktion ist, tippe zunächst folgenden Sourcecode ab. Erstelle dazu ein neues Projekt oder benutze einfach das aus dem ersten Blog. Wichtig ist, dass du den folgenden Sourcecode in die Datei main.swift schreibst.

func addiere(zahl1: Double, zahl2: Double) -> Double {
   return zahl1 + zahl2
}


Eine Funktion erkennt man daran, dass sie mit dem Schlüsselwort func beginnt. Damit weiß der Swift-Compiler, dass jetzt die Definition einer Funktion beginnt. Dann folgt der Name der Funktion. Hier ist es addiere.

Der darauf folgende Bereich zwischen den runden Klammern (zahl1: Double, zahl2: Double) gibt kommasepariert die Parameter an, die der Funktion übergeben werden. Ein Parameter – oft auch Argument – genannt, ist also ein Wert, der einer Funktion übergeben wird. Die Funktion kann dann auf die Parameter zugreifen. Im Beispiel werden zwei Parameter übergeben: der Parameter zahl1 vom Typ Double und der Parameter zahl2 ebenfalls vom Typ Double. Der Typ Double repräsentiert eine Fließkommazahl. Eine Beschreibung solcher Standardtypen findest du im folgenden Exkurs “Standardtypen".

Swift kennt eine Reihe von Standardtypen, die im Folgenden kurz beschrieben werden.

  • Ganze Zahlen: Swift kennt eine ganze Reihe von Typen, die ganze Zahlen repräsentieren. Darunter fallen Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64 und schließlich Int und UInt. Alle aufgezählten Typen bis auf Int und UInt unterscheiden sich in der Menge an Zahlen, die sie repräsentieren können. Das U bei UInt8 bis UInt64 steht für unsigned und bedeutet, dass nur positive Zahlen und 0 erlaubt sind. Die restlichen Typen (ohne vorangestelltes U) können sowohl negative als auch positive Zahlen sowie die 0 speichern. In den meisten Fällen ist es nicht notwendig einen der Typen mit Größenangabe zu benutzen. Es reicht oftmals aus, den Typ Int oder UInt zu benutzen. Beide Typen haben immer eine plattformabhängige maximale Größe, also Int32, Int64, UInt32 oder UInt32. Für den Anfang soll es reichen, immer Int zu benutzen. Wenn Du wissen möchtest, welche die maximalen und minimalen Zahlen sind, die ein konkreter Typ akzeptiert, kannst Du die Variablen min und max abfragen: print(“Int32 - Min = \(Int32.min) Max = \(Int32.max)”)
  • Fließkommazahlen: Fließkommazahlen werden in Swift durch die Typen Double und Float repräsentiert. Double kann mehr Nachkommastellen repräsentieren und ist daher präziser. Üblicherweise wird Double genutzt, sobald man eine Fließkommazahl speichern muss.
  • Wahrheitswerte: Der Typ Bool ist ein Wahrheitswerttyp. Das heißt, eine Variable vom Typ Bool kann nur zwei Werte einnehmen, entweder true (wahr) oder false (falsch).
  • Zeichenkette: Eine Zeichenkette ist eine Reihe von Zeichen, die für sich eine Einheit bilden. Eine Zeichenkette wird in Swift durch den Typen String repräsentiert. Will man eine Zeichenkette definieren, so nutzt man Gänsefüßchen, um die Grenzen der entsprechenden Zeichenkette zu setzen. Hier ein Beispiel: “Mein Name ist Hase.”.

Wenn eine Funktion nach Beendigung ihrer Berechnung einen Rückgabewert zurückgeben möchte, wird das mit dem Zeichen -> und dem Rückgabetyp, hier Double, angegeben. Eine andere Funktion, die beispielsweise den Nachnamen einer Person zurückgeben würde, müsste den Rückgabetyp folgendermaßen definieren: -> String.

Nach dem Rückgabetyp folgt der sogenannte Body einer Funktion, welcher durch das Klammernpaar { und } umfasst wird. Somit ist der Sourcecode, der zwischen den geschweiften Klammern steht, das, was die Funktion ausmacht. In unserem Beispiel werden die Parameter zahl1 und zahl2 addiert und das Ergebnis mithilfe des Schlüsselworts return zurückgegeben. Eine detaillierte Beschreibung von Funktionen findest Du im Bereich “Funktionen”.
 

Definition einer Funktion

In Swift ist eine Funktion ein Stück Sourcecode, der eine spezifische Aufgabe erfüllt. Betrachten wir als Beispiel folgenden Sourcecode.

func say(text: String, person: String) -> String {
   let sentence = "Hi \(person), \(text)"

   return sentence
}


Wir haben hier eine sehr simple Funktion mit dem Namen say, die nichts weiter tut, als einen Gruß an eine Person zu erzeugen und zurückzugeben. Sie besitzt daher Eingabeparameter und liefert am Ende einen Rückgabewert. Im nächsten Bild habe ich die verschiedenen Teile einer Funktion farblich hervorgehoben.

Funktion-Teile

Ok, dann fangen wir mal an, eine Funktion zu erklären. Eine Funktion erkennt man daran, dass sie mit dem Schlüsselwort func beginnt. Damit weiß der Swift-Compiler, dass jetzt die Definition einer Funktion beginnt. Dann folgt der Name der Funktion. Hier ist es say. Im übrigen sollte im Funktionsnamen immer ein Verb enthalten sein, da er eine Tätigkeit repräsentiert. Das ist das Wesen einer Funktion; sie tut etwas.

Der darauf folgende Bereich zwischen den runden Klammern (text: String, person: String) gibt kommasepariert die Parameter an, die der Funktion übergeben werden, wenn sie aufgerufen wird, d.h., wenn sie ausgeführt wird. Wie eine Funktion aufgerufen wird, erkläre ich gleich. Mit Parametern, auch Argumente genannt, sind namengebundene Werte gemeint, die die Funktion benötigt, um ihre Aufgabe erledigen zu können. Im oberen Beispiel sind zwei Parameter deklariert: text ist der erste Parameter vom Typ String, der zweite Parameter lautet person und ist ebenfalls vom Typ String. Die Schreibweise für die Deklaration eines Parameters lautet also Parametername Doppelpunkt Parametertyp. Im Falle, dass eine Funktion keine Parameter benötigt, bleibt der Bereich zwischen den runden Klammern eben leer. Solch eine Funktion könnte dann folgendermaßen aussehen: func say() -> String { … }.

Der Bereich zwischen den geschweiften Klammern { … } nennt man Body und enthält den Sourcecode, der die Funktion ausmacht. In dieser Funktion wird im Body zunächst eine Konstante sentence definiert und deren Inhalt mithilfe des Schlüsselworts return zurückgegeben; der zurückgegebene Wert befindet sich dabei rechts vom return Befehl. Wenn also eine Funktion einen Rückgabetyp deklariert, muss auch im Body die zuletzt ausgeführte Anweisung der return Befehl sein. Und was ist, wenn die Funktion überhaupt keinen Rückgabewert liefern möchte? Nun, ganz einfach, dann deklariert man oben keinen Rückgabetyp. Die Funktion würde dann folgendermaßen aussehen: func say() { … }. In diesem Fall darf auch kein return im Body enthalten sein.


Aufruf von Funktionen

Jetzt möchte ich noch kurz erklären, wie eine Funktion aufgerufen wird. Angenommen die Funktion, die Du aufrufen möchtest, erwartet keine Parameter, dann schreibst Du den Namen der Funktion gefolgt von (). Hier ein Beispiel: say(). Wenn die Funktion jedoch Parameter erwartet, wie im oberen Beispiel, sieht der Aufruf folgendermaßen aus: say(text: “How are you?”, person: “Mary”).

Für jeden erwarteten Parameter gilt: Erst wird der Parametername angegeben, dann ein Doppelpunkt und schließlich der zu übergebende Wert. Hier hat z.B. der Parameter text den Wert “How are you?” und der Parameter person den Wert “Mary”. Und nicht vergessen: Parameterangaben werden jeweils mit einem Komma getrennt.

Funktionsaufrufe können auch innerhalb von anderen Funktionsaufrufen erfolgen. Das kann ich am Besten an einem Beispiel erklären:

print(say(text: "How are you?", person: "Mary"))>


Hier wird die Funktion print aufgerufen. Als Parameter wird nicht direkt eine Zeichenkette (String) übergeben. Stattdessen wird die Funktion say aufgerufen und deren Rückgabewert wird schließlich an die Funktion print weitergegeben. Das funktioniert, da der Rückgabetyp von say gleichzeitig der von print erwartete Parametertyp ist. Man könnte sich auch Folgendes vorstellen:

func giveMeAGreet() -> String {
   let greet = "Is it you?"

   return greet
}


Hier wird der als text übergebene Wert erst durch den Funktionsaufruf giveMeAGreet() ermittelt.


Ignorierte Rückgabetypen

Wenn eine Funktion einen Rückgabetyp definiert, muss beim Aufruf der zurückgelieferte Wert aufgefangen werden. Hier ein Beispiel:

let greet = say(text: "How are you?", person: "Mary")

Hier wird der Rückgabewert der Funktion say in der Konstanten greet gespeichert. Würdest Du say aufrufen, ohne den Rückgabewert auf irgendeine Art zu verwenden, bekämest Du die Warning Result of call to 'say(text:person:)' is unused. Nur zur Erklärung: Eine Warning ist kein Fehler, jedoch etwas, das ein Fehler sein könnte. Der Compiler gibt Dir also einen Hinweis, was eventuell korrekt programmiert ist. Du kannst es ja versuchen. Entferne aus der obigen Anweisung die Zuweisung zu der Konstante greet.

say(text: "How are you?", person: "Mary")


Nun, was kannst Du denn machen, wenn Du tatsächlich keine Verwendung für den Rückgabewert hast und dennoch nicht diese Warning bekommen möchtest? Swift hat dafür etwas vorgesehen. Du hast die Möglichkeit, explizit anzugeben, dass Du den Rückgabewert nicht benötigst. Das sieht dann folgendermaßen aus:

let _ = say(text: "How are you?", person: "Mary")


Du musst lediglich den Rückgabewert der Konstanten _ zuweisen.


Weitere Unterthemen

Zu Funktionen gibt es noch eine Menge Themen, die ich hier beschreiben könnte. Ich denke jedoch, dass es zu früh ist, um zu sehr in die Tiefe zu gehen. Aber keine Sorge, alles wird im Laufe der nächsten Blogs nachgeliefert.

Um zu sehen, wie diese Funktion jetzt funktioniert, tippe folgende Zeilen ein:

let summe = addiere(zahl1: 1, zahl2: 1)

print("1 + 1 = \(summe)")


In der ersten Zeile wird die Konstante summe definiert und mit dem Ergebniswert des Funktionsaufrufs addiere(zahl: 1, zahl2: 1) gesetzt. Ach ja, eine Funktion muss natürlich aufgerufen werden, damit sie den Rückgabewert berechnet. Im Beispiel (s.o.) kannst du sehen, wie eine Funktion aufgerufen wird. Zuerst wird der Funktionsname angegeben, danach die Parameterwerte. Wir erinnern uns: Die Parameterdeklaration (zahl1: Double, zahl2: Double) enthält die Parameternamen und deren Typen. Beim Aufruf werden die Typen durch konkrete Werte ersetzt – hier (zahl1: 1, zahl2: 1). Der berechnete Wert wird dann der Konstante summe zugewiesen. Schließlich bringt die print Funktion die Wahrheit ans Licht und zeigt, dass summe = 2.0 ist.

So, jetzt, da wir wissen, wie eine einfache Funktion erstellt werden kann, programmieren wir die restlichen drei Funktionen für Multiplikation, Division, und Subtraktion nach dem gleichen Muster. Tippe folgende Zeilen ein:

func subtrahiere(zahl1: Double, zahl2: Double) -> Double {
    return zahl1 - zahl2
}

func multipliziere(zahl1: Double, zahl2: Double) -> Double {
    return zahl1 * zahl2
}

func dividiere(zahl1: Double, zahl2: Double) -> Double {
    return zahl1 / zahl2
}

let summe          = addiere(zahl1: 1, zahl2: 1)
let subtraktion    = subtrahiere(zahl1: 4, zahl2: 1)
let multiplikation = multipliziere(zahl1: 2, zahl2: 3)
let division       = dividiere(zahl1: 12.5, zahl2: 2.5)

print("1 + 1      = \(summe)")
print("4 - 1      = \(subtraktion)")
print("2 + 3      = \(multiplikation)")
print("12.5 / 2.5 = \(division)")

 
Führe das Programm aus und Du wirst folgende Ausgaben sehen:

1 + 1     = 2.0

4 - 1     = 3.0

2 + 3     = 6.0

12.5 / 2.5 = 5.0


Das ist doch schon mal super. Ach, übrigens: Fließkommazahlen werden in Swift mit einem Punkt geschrieben und nicht wie in der Mathematik üblich mit einem Komma. Es muss also heißen 12.5 und nicht 12,5.

Mathematische Operationen verallgemeinern

Aber nun weiter im Text. Unsere Anforderung zu Beginn war, eine Möglichkeit zu finden, wie mathematische Operationen verallgemeinert werden können. Wir haben es geschafft, die vier wichtigsten mathematischen Operatoren nachzubilden. Doch lass uns einen tieferen Blick in unsere Errungenschaften nehmen. Unsere Beispiele waren bisher sehr einfacher Natur. Doch wie sieht es aus mit folgendem Ausdruck aus: (5 - 2) + (3 * (12 / 2)).
Und, schon eine Idee?

Nun, das ist gar nicht so schwer. Tippe einfach folgende Zeilen ab:

let ergebnis1 = addiere(zahl1: subtrahiere(zahl1: 5, zahl2: 2), zahl2: multipliziere(zahl1: 3, zahl2: dividiere(zahl1: 12, zahl2: 2)))
print("Ergebnis von (5 - 2) + (3 * (12 / 2)) = \(ergebnis1)")

Führe das Programm aus und du wirst folgen Ausgabe sehen: Ergebnis von (5 - 2) + (3 * (12 / 2)) = 21.0


Wie du siehst, können Funktionsaufrufe als Parameter benutzt werden. Und warum auch nicht? Wenn ein Funktionsaufruf A einen weiteren Funktionsaufruf B als Parameter enthält, muss zunächst der Funktionsaufruf B ausgeführt werden, damit der Funktionsaufruf A möglich ist. Im oberen Beispiel wird daher subtrahiere(zahl1: 5, zahl2: 2) zuerst ausgeführt. Das Ergebnis dieses Funktionsaufrufes ist 3, was dann als Wert des Parameters zahl1: der Funktion addiere übergeben wird. Das ist doch ein cooler Nebeneffekt, oder? Durch die Verschachtelung der Funktionsaufrufe sind wir in der Lage, Klammern und somit Berechnungsreihenfolgen nachzubilden. 

Fassen wir zusammen: In diesem Blog haben wir Funktionen kennengelernt. Mit ihrer Hilfe können wir mathematische Operationen nachbilden und sind damit in der Lage, beliebige Zahlen einzusetzen. Von den zwei beschriebenen Punkten zu Beginn des Blogs haben wir also den zweiten geknackt. Im nächsten Blog werden wir uns daran machen, eine Lösung für Punkt 1– also die Frage wie ein Programm aus einer Zeichenkette erkennen kann, was es berechnen soll – zu finden. 

Über Juan Carlos Flores

Juan Carlos Flores ist seit 2006 Software Architekt bei itemis. Als diplomierter Informatiker wirkt er seit 20 Jahre in vielen Enterprise-Projekten mit und trägt zu Problemlösungen bei.