Software Development, Smart Technologies, Apps

Swift-Tutorial Teil 5: Das geht noch besser

Hallo zusammen! In diesem Teil unseres Swift Tutorials werden wir eine Menge neuer Sprachkonstrukte kennenlernen, die unabdinglich sind, damit guter Source Code überhaupt entstehen kann. Ja genau, guter Sourcecode. Denn bekanntlich führen viele Wege nach Rom und darunter verstecken sich einige, die man lieber nicht gehen sollte. Aber genug der Metaphern. Richten wir unsere Aufmerksamkeit auf die Frage: Was macht Source Code zu gutem Source Code?

Die 3 wichtigsten Tipps für sauberen Source Code

Beginnen wir mit drei Kriterien:

  1. Don’t ignore warnings: Moderne Entwicklungsumgebungen, wie Xcode, geben Programmierern Hinweise auf problematischen Source Code, der zur Laufzeit zu Fehlern führen könnte. Diese Warnungen zu ignorieren ist schlicht fahrlässig. Das ist so, als würde man die leuchtende Ölstandsanzeige im Auto sehen und dennoch entspannt weiterfahren. Also, auch wenn es lästig ist, Warnungen haben ihren Sinn und sollten immer beachtet werden.
  2. Don’t repeat yourself (Dry): Wiederhole dich nicht selbst, bedeutet schlicht, dass gleicher oder fast gleicher Source Code gemieden werden sollte. Wenn in einem Programm irgendeine Tätigkeit an verschiedenen Stellen ausgeführt werden muss, sollte der entsprechende Source Code nur ein mal vorhanden sein. Etwas technischer ausgedrückt: Vermeide Redundanz in deinem Source Code.
  3. Lesbarkeit: Ja es ist richtig: Ein Programm wird von einer Maschine ausgeführt. Was hilft es da, wenn ich ausschweifend lange Variablennamen benutze und mir auch sonst sehr viel Mühe gebe, den Source Code lesbar zu gestalten? Ich weiß doch, was ich mit allem meine und der Rechner kommt eh damit klar. Nun das stimmt nur, wenn du ein perfektes Gedächtnis hast. Und glaub mir, das hat niemand. Es wird der Tag kommen, an dem du einen Fehler beseitigen willst oder eine Erweiterung programmieren möchtest. Dann ist es wichtig, den alten, schlecht lesbaren Source Code zu verstehen. Oder denk an die absolut realistische Situation, dass mehrere Programmierer am selben Source Code arbeiten müssen. Denk einfach daran: Du schreibst nicht für dich, sondern für denjenigen, der nach dir den Source Code lesen muss.

Natürlich gibt es noch mehr Kriterien oder Prinzipien, nach denen man sich richten sollte, wenn man programmiert. Ich werde im Laufe der nächsten Blog-Beiträge im Rahmen der Swift Tutorial Reihe weitere vorstellen. Ich habe mich entschlossen auf diese Punkte einzugehen, weil ich der Meinung bin, dass man einen guten Programmierstil nicht früh genug lernen kann.

Dir Swift beizubringen und dich dann damit allein zu lassen, entspricht nicht meiner Vorstellung eines Tutorials. Deswegen werden wir uns in diesem Teil damit beschäftigen, die obigen Kriterien auf den aktuellen Stand unseres Source Codes anzuwenden.

Rakete-Smartphone-App-Swift

 

Optimierung unseres Swift Tutorial Source Codes

Gut, dann schauen wir uns doch mal den Source Code an, den wir am Ende in Swift Tutorial Teil 4 geschrieben haben:

func compute(expression : String) -> Double? {
    if let operatorIndex  = getOperatorIndex(expression) {
        let operatorSymbol = expression[operatorIndex]
        let leftUntrimmed  = expression[..<operatorIndex]
        let leftStr        = leftUntrimmed.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
        let left           = Double(leftStr)!
        let rightUntrimmed = expression[expression.index(after: operatorIndex)..<expression.endIndex]
        let rightStr       = rightUntrimmed.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
        let right          = Double(rightStr)!

        if (operatorSymbol == "+") {
            return add(value1: left , value2: right)
        }

        if (operatorSymbol == "-") {
            return subtract(value1: left , value2: right)
        }

        if (operatorSymbol == "*") {
            return multiply(value1: left , value2: right)
        }

        return divide(value1: left , value2: right)
    }

    return nil
}

func getOperatorIndex(_ expression : String) -> String.Index? {
    var index  = expression.index(of: "+")
    
    if (index == nil) {
        index  = expression.index(of: "-")
        
        if (index == nil) {
            index  = expression.index(of: "*")
            
            if (index == nil) {
                index  = expression.index(of: "/")
            }
        }
    }
    
    return index
}

func add(value1: Double, value2: Double) -> Double {
    return value1 + value2
}

func subtract(value1: Double, value2: Double) -> Double {
    return value1 - value2
}

func multiply(value1: Double, value2: Double) -> Double {
    return value1 * value2
}

func divide(value1: Double, value2: Double) -> Double {
    return value1 / value2
}

let myAddition = "20 + 7"
let mySubtraction = "35 - 8"
let myMultiplication = "13.5 * 2"
let myDivision = "54 / 2"
let myWrongAddition = "20 = 7"

let additionResult = compute(expression: myAddition)
let subtractionResult = compute(expression: mySubtraction)
let multiplicationResult = compute(expression: myMultiplication)
let divisionResult = compute(expression: myDivision)
let wrongAdditionResult = compute(expression: myWrongAddition)

print("Die Summe \(myAddition) ist gleich \(additionResult)")
print("Die Subtraktion \(mySubtraction) ist gleich \(subtractionResult)")
print("Die Multiplikation \(myMultiplication) ist gleich \(multiplicationResult)")
print("Die Subtraktion \(myDivision) ist gleich \(divisionResult)")
print("Die fehlerhafte Summe \(myWrongAddition) ist gleich \(wrongAdditionResult)")

 

Wir kümmern uns zunächst um die Warnungen, die sich auf die print Anweisungen beziehen. Sie bieten uns zwei mögliche Lösungen an:

  1. Use 'String(describing:)' to silence this warning
  2. Provide a default value to avoid this warning

Die erste Warnung schlägt vor, den Initializer String(describing:) zu benutzen. Lass uns das einfach versuchen.

print("Die Summe \(myAddition) ist gleich \(String(describing: additionResult))")
print("Die Subtraktion \(mySubtraction) ist gleich \(String(describing: subtractionResult))")
print("Die Multiplikation \(myMultiplication) ist gleich \(String(describing: multiplicationResult))")
print("Die Subtraktion \(myDivision) ist gleich \(String(describing: divisionResult))")
print("Die fehlerhafte Summe \(myWrongAddition) ist gleich \(String(describing: wrongAdditionResult))")

 

Der Initializer String(describing:) erzeugt eine Zeichenkette (String) für den übergebenen Wert. Wenn der Wert ein "Optional" ist, wird der eingepackte Wert ausgelesen und eine Zeichenkette für ihn erzeugt und zurückgeliefert. Mit dieser Vorgehensweise wird auch für die letzte Konstante wrongAdditionResult, die ja "nil" ist, ein String erzeugt, der den Wert nil besitzt. Das bedeutet, dass der Initializer uns die Arbeit abnimmt, einen Wert auszupacken bzw. zu überprüfen, ob dieser nil ist. Diese Lösung führt eindeutig dazu, dass die Warnungen alle verschwinden. Mal sehen, ob es eine bessere Lösung gibt.

Die zweite Möglichkeit nutzt den sogenannten Nil-Coalescing Operator ??, um in dem Fall, dass der Wert einer der Konstanten nil ist, einen (anderen) Standardwert auszugeben.

Nil-Coalescing Operator

Der Nil-Coalescing-Operator (a ?? b) entpackt ein optionales a, wenn es einen Wert enthält, oder gibt einen Standardwert b zurück, wenn a gleich nil ist. Dabei muss a immer ein Optional sein. Der Ausdruck b muss mit dem Typ übereinstimmen, der in a gespeichert ist.

Im Grunde ist (a ?? b) eine kompakte Darstellung für folgenden Sourcecode:

if (a != nil) {
    a = a!
}
else {
    a = b
}

 

Mithilfe des Nil-Coalescing-Operators kann man sehr elegant Standardwerte definieren. Betrachte dazu das nächste Beispiel:

let defaultLevel = 27
var userDefinedLevel: Int?   // Deklaration ohne Initialisierung. Ist also nil

var startLevel = userDefinedLevel ?? defaultLevel

 

Die Variable startLevel wird standardmäßig auf defaultLevel gesetzt. Nur wenn die Variable userDefinedLevel einen Wert besitzt, erhält startLevel den Wert von userDefinedLevel.

 


Betrachte dazu den folgenden Source Code:

print("Die Summe \(myAddition) ist gleich \(additionResult ?? -9999.0)")
print("Die Subtraktion \(mySubtraction) ist gleich \(subtractionResult ?? -9999.0)")
print("Die Multiplikation \(myMultiplication) ist gleich \(multiplicationResult ?? -9999.0)")
print("Die Subtraktion \(myDivision) ist gleich \(divisionResult ?? -9999.0)")
print("Die fehlerhafte Summe \(myWrongAddition) ist gleich \(wrongAdditionResult ?? -9999.0)")

 

Du wirst dich sicherlich fragen, warum der Standardwert 9999.0 ist. Wenn man ehrlich ist, muss man darauf antworten: Weil diese Umsetzung schlecht ist. Im Ernst, ich habe sie hier aufgeführt, weil ich dir ein Problem erklären will, und zwar, dass nicht jede Lösung, die Xcode anbietet, in jeder Situation angemessen ist.

In diesem Fall macht ein Standardwert tatsächlich keinen Sinn. Oder kannst du mir sagen, was ein Standardergebnis einer beliebigen Addition, die auch noch fehlerhaft ist, sein kann? Natürlich nicht, und zwar weil es keinen Standardwert für eine Rechnung gibt. Es gibt entweder einen Lösungswert oder der arithmetische Ausdruck war fehlerhaft. Letzterer Fall führt zu nil und nicht zu einem Standardwert.

Obwohl, es gibt eine Ausnahme: Division durch 0. Dieser Fall führt zwar auch nicht zu einem Wert, dennoch sollte eine Unterscheidung zwischen fehlerhafter Eingabe (z.B. ein unerlaubtes Zeichen) und der Division durch 0 gemacht werden.
 

Optional binding in Swift eine Alternative zu printf

Ok, jetzt haben wir beide Möglichkeiten gesehen und festgestellt, dass nur die erste tatsächlich sinnvoll ist. Ich möchte dir in diesem Swift Tutorial aber noch eine weitere Alternative vorstellen. Dazu ersetze ich sowohl die Definitionen der Konstanten als auch die Print-Ausgaben durch folgenden Source Code:

if let additionResult = compute(expression: myAddition) {
    print("Die Summe \(myAddition) ist gleich \(additionResult)")
}

if let subtractionResult = compute(expression: mySubtraction) {
    print("Die Subtraktion \(mySubtraction) ist gleich \(subtractionResult)")
}

if let multiplicationResult = compute(expression: myMultiplication) {
    print("Die Multiplikation \(myMultiplication) ist gleich \(multiplicationResult)")
}

if let divisionResult = compute(expression: myDivision) {
    print("Die Subtraktion \(myDivision) ist gleich \(divisionResult)")
}

if let wrongAdditionResult = compute(expression: myWrongAddition) {
    print("Die fehlerhafte Summe \(myWrongAddition) ist gleich \(wrongAdditionResult)")
}

 

Betrachte zunächst nur die ersten drei Zeilen. Mithilfe des "Optional Bindings" haben wir folgende drei Dinge erreicht:

  1. die Konstante additionResult ist kein Optional mehr. Sie enthält immer den ausgepackten Wert.
  2. Dadurch muss im Source Code zwischen den geschweiften Klammern nie der Wert ausgepackt werden, es ist ja schon längst geschehen.
  3. Die Konstante additionResult ist nur im Source Code zwischen den geschweiften Klammern bekannt.

Das gleiche gilt analog für die anderen Optional Bindings.

Vielleicht ist dir aufgefallen, dass bei Ausführung des Programms für die fehlerhafte Addition, deren Ergebnis in der Konstante wrongAdditionResult gespeichert ist, keine Ausgabe erfolgt. Das liegt natürlich daran, dass der Funktionsaufruf compute(expression: myWrongAddition) nil zurück liefert.

Wäre doch schön, wenn man diesen Umstand zu den Print-Ausgaben hinzufügen könnte. Nun, nichts einfacher als das. Zu jedem Optional Binding, können wir jetzt komfortabel einen Else-Zweig einfügen. Das sieht für die Konstante wrongAdditionResult dann folgendermaßen aus:

if let wrongAdditionResult = compute(expression: myWrongAddition) {
    print("Die fehlerhafte Summe \(myWrongAddition) ist gleich \(wrongAdditionResult)")
} else {
    print("Die fehlerhafte Summe \(myWrongAddition) hat natürlich kein Ergebnis.")
}

 

Betrachten wir als nächstes die Funktion getOperatorIndex(_ expression : String):

func getOperatorIndex(_ expression : String) -> String.Index? {
    var index = expression.index(of: "+")

    if (index == nil) {
        index = expression.index(of: "-")

        if (index == nil) {
            index = expression.index(of: "*")

            if (index == nil) {
                index = expression.index(of: "/")
            }
        }
    }

    return index
}

 

Diese Funktion könnte man auch wie folgt schreiben:

func getOperatorIndex(_ expression : String) -> String.Index? {
    if let index = expression.index(of: "+") {
      return index
    }

    if let index = expression.index(of: "-") {
        return index
    }

    if let index = expression.index(of: "*") {
        return index
    }

    if let index = expression.index(of: "/") {
        return index
    }

    return nil
}

 

Welche der zwei Varianten gefällt dir besser? Mir gefällt die zweite Variante besser. Ich kann sie schlicht besser lesen. Aber ganz ehrlich, das sind Feinheiten. Vielleicht kann man damit argumentieren, dass die erste Variante sich ungünstig verhält, wenn man weitere Operatoren hinzufügt. In diesem Fall würden die Einrückungen irgendwann die Lesbarkeit stören.
 

Ein weiterer Ansatz hin zu sauberem Quellcode

Es gibt noch eine weitere Variante, die meiner Meinung nach echte Vorteile hat und die in diesem Tutorial nicht fehlen darf. Zuvor solltest du dir jedoch zwei Themen durchlesen:

Array

Ein Array repräsentiert eine geordnete Sammlung von Werten eines festgelegten Typs. Geordnet bedeutet, dass jeder im Array gespeicherte Wert eine feste Position hat, sodass die Werte immer in der gleichen Reihenfolge ausgelesen werden können. Dabei kann derselbe Wert mehrmals auf unterschiedlichen Positionen vorkommen.

Swift Tutorial Array

Betrachte nun folgendes Beispiel:

var anArray = Array<Int>()

 

Der Typ Array<Int> bezeichnet ein Array, dass nur Werte des Typs Int aufnimmt. Der Ausdruck Array<Int>() ist folglich der Initializer für den Typ Array<Int>. Also eine spezielle Funktion, die einen Wert des angegebenen Typs zurück liefert. Was unter einem Initializer genau zu verstehen ist, habe ich bereits in den vorangegangenden Blogbeiträgen beschrieben. Was hier aber neu ist, ist der zwischen spitzen Klammern eingefasste Typ Int, also <Int>. Es handelt sich hierbei um eine spezielle Notation, die zur Nutzung und Beschreibung von sogenannten Generics dient. Auf diese sehr mächtige Eingenschaft Swifts werde ich in zukünftigen Blogbeiträgen eingehen. An dieser Stelle solltest Du Dir nur merken, dass Array<Int> nichts anderes bedeutet als ein Array, dass nur Werte des Typs Int speichern kann. Wolltest Du ein Array definieren, dass nur Strings verwalten kann, so solltst Du den Typ Array<String> nutzen.

var anotherArray = [Int]() 

 

Es gibt eine vereinfachte Form, um ein Array zu erstellen. Betrachte das obere Beispiel. Dort wird ein Array mithilfe von [Int]() erstellt. Die Kurzform [Int]() ist äquivalent zu Array<Int>(). Obwohl beide Formen funktionell identisch sind, wird die Kurzform bevorzugt und in diesem Tutorial verwendet, wenn es um den Typ eines Arrays geht.

Sowohl anArray als auch anotherArray sind Arrays, die leer sind. Um ein Array mit Werten zu füllen, bietet der Typ Array eine Funktion mit dem Namen append(…). Um anArray mit Werten zu füllen, kannst Du folgende Anweisungen ausführen:

anArray.append(9)
anArray.append(12)
anArray.append(15)

print(anArray)

 

Es gibt allerdings eine kompaktere Form, um ein Array initial zu füllen. Du kannst ein Array auch mit einem Array-Literal initialisieren [Wert1, Wert2, Wert3 , …]. Ein Array-Literal wird als Liste von Werten geschrieben, die durch Kommata getrennt und von einem Paar eckiger Klammern umgeben sind. Hier zwei Beispiele:

let weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
var friends  = ["Frodo", "Sam", "Pipin", "Merry"]

 

Sowohl die Konstante weekday als auch die Variable friends sind vom Typ Array<String> (oder auch [String]). Beachte aber, dass nur das Array friends/ erweitert bzw. verändert werden kann, da es als Variable definiert ist. Mann kann natürlich auch den Typ explizit angeben, allerdings nur für die kompakte Form mit [Typ]. Siehe das folgenden Beispiel:

let weekdays : [String] = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
var friends  : [String] = ["Frodo", "Sam", "Pipin", "Merry"]

 

Um Elemente aus einem Array auszuwählen, kannst Du die sogenannte Subscript-Syntax nutzen. Diese sieht folgendermaßen aus: Name der Variablen oder Konstanten, gefolgt von einer öffnenden eckigen Klammer, gefolgt von der Position des gewünschten Elements und schließlich eine schließende eckige Klammer. Beachte bitte, dass in Arrays die erste Position 0 ist und nicht 1. Ein Beispiel ist weekdays[2] (also das dritte Element), um den String “Wednesday” aus dem Array zu lesen.

let theThirdDay = weekdays[2]

 

Du kannst, wie oben erwähnt, mithilfe der Funktion append(…) ein weiteres Element dem Array hinzufügen. Es gibt jedoch noch eine andere Möglichkeit, die unter Umständen besser geeignet ist. Betrachte dazu das nächste Beispiel:

friends += ["Bilbo", "Meriadoc", "Gandalf", "Aragorrrn", "Boromir", "Legolas", "Gimli"]

print(friends)

 

Wie Du siehst, ist es nicht notwendig die Funktion append(…) 7 mal aufzurufen, um 7 neue Freunde hinzuzufügen. Der Operator += addiert alle Elemente aus dem zugewiesenen Array in das Array friends. Es ist auch möglich ein Element aus dem Array gegen ein neues auszutauschen. Wie Dir vielleicht aufgefallen ist, habe ich oben den lieben Aragorn falsch geschrieben. Lass uns das ändern. Hier der Sourcecode:

friends[7] = "Aragorn"

print(friends)

 

Beachte aber, dass Du einen Laufzeitfehler bekommen wirst wenn Du auf ein Element zugreifen willst, dass nicht existiert. Der Aufruf print(friends[20]) wird sicher zu einem Fehler führen, da Dein Array ja nur 11 Elemente enthält.

Du kannst auch mehrere zusammenhängend benachbarte Elemente auf einmal ändern, auch wenn die Ersetzungsmenge eine andere Länge hat als die zu ersetzende Menge. Das folgende Beispiel ersetzt alle Hobbits durch einen neuen String “5 Hobbits”:

friends[0...4] = ["5 Hobbits"]

print(friends)

 

Um Elemente aus einem Array zu entfernen, können wir die Funktionen remove(at:), removeFirst() oder removeLast() benutzen. Erstere entfernt das Element an der übergebenen Position, die beiden anderen Funktionen jeweils das erste oder das letzte Element des Arrays. Die Funktionen selbst liefern als Rückgabewert das Entfernte Element.

let hobbits = friends.removeFirst()
print(friends)

let gimli = friends.removeLast()
print(friends)

let gandalf = friends.remove(at: 1)
print(friends)

print(hobbits)
print(gimli)
print(gandalf)

 

For-In-Schleife

Manchmal möchte man alle Elemente einens Arrays nacheinander verarbeiten. Die For-In-Schleife ist bestens dafür geeignet. Betrachte dazu folgendes Beispiel:

let planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]

for planet in planets {
    print(planet)
}

print("Alle Planetennamen sind ausgegeben worden.")

 

Eine For-In-Schleife beginnt immer mit den Schlüsselwort for. Danach kommt eine sogenannte Laufvariable, gefolgt von einem Wert des Typs Set, Array oder Dictionary. Tatsächlich sind auch andere Typen möglich. Die genannten sollen hier jedoch reichen, da sie die verbreitetsten sind. Schließlich kommt ein Block an Sourcecode, der von geschweiften Klammern { … } umschlossen ist.

Was hier passiert, ist Folgendes: Die Laufvariable planet bekommt den Wert des ersten Elements aus dem Set planets. Also planet = “Mercury”, wie in der Grafik zu sehen ist.

Swift Tutorial For-In-Schleife

Nachdem also die Laufvariable planet einen Wert bekommen hat, wird der gesamte Sourcecode zwischen den geschweiften Klammern { … } ausgeführt. In diesem Beispiel besteht der Sourcecode nur aus einer Zeile print(planet). In der wirklichen Welt wird der Anweisungsblock einer Schleife natürlich wesentlich komplexer sein. In unserem Beispiel wird schließlich der Wert der Varibalen planet ausgegeben.

Beim ersten Durchlauf wird Mercury ausgegeben. Nachdem die letzte Zeile des Sourcecodes innerhalb der geschweiften Klammern ausgeführt worden ist, beginnt der nächste Durchlauf (In unserem Beispiel ist es eh nur eine Zeile). Dazu wird der Laufvariablen planet der nächste Wert des Sets planets zugewiesen. Beim ersten Durchlauf war es Mercury, jetzt ist Venus dran. Und wieder wird der Sourcecode innerhalb der geschweiften Klammern ausgeführt. Diesmal ist die Ausgabe Venus. Danach wird der gleiche Vorgang mit dem nächsten Element Earth ausgeführt. Irgendwann wird der Laufvariable planet der Wert Neptune zugewiesen und der Anweisungsblock ausgeführt. Das ist der Moment, da zu jedem Element im Set planets ein Schleifendurchlauf durchgeführt worden ist. Die Abarbeitung der Schlaife ist beendet und die nächste Anweisung nach der Schleife wird ausgeführt.

Beachte bitte, dass die Laufvariable planet automatisch vom Typ des im zugewiesenen Wertes ist. In unserem Beispiel ist daher planet immer vom Typ String.

Das obere Beispiel haben wir mit einem Array durchgeführt, weil wir ein Array-Literal für die Zuweisung benutzt haben. Gleichzeitig wurde der Typ der Variablen planets aus dem zugewiesenem Wert abgeleitet. Hier also ein Array<String>. Jetzt wollen wir uns kurz ansehen, wie eine For-In-Schleife aussieht, wenn wir über ein Set iterieren wollen. Dazu brauchen wir nur im Beispiel den Typ explizit auf Set<String> zu setzen:

var planets : Set<String> = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]

for planet in planets {
    print(planet)
}

print("Alle Planetennamen sind ausgegeben worden.")

 

Hier fallen gleich zwei Dinge auf. Erstens, die For-In-Schleife unterscheidet sich nicht von der Schleife für ein Array. Dazu brauchen wir nichts weiter zu sagen, denke ich. Und zweitens, die Elemente aus planets werden in einer anderen Reihenfolge abgearbeitet. Das liegt daran, das ein Set keine definierte Reihenfolge besitzt. Diesen Umstand solltest Du nie vergessen.

Bleibt nur noch zu beschreiben, wie eine For-In-Schleife bei Dictionarys aussieht. Betrachte dazu das folgende Beispiel:

let elements = ["Al" : "Aluminium", "H" : "Wasserstoff" , "O" : "Sauerstoff", "Cr" : "Chrom"]

for (kuerzel, name) in elements {
    print("Das Element \(name) hat das Kürzel \(kuerzel)")
}

 

Wie Du siehst, haben wir nicht mehr nur eine Laufvariable, sondern gleich zwei; eine für den Schlüssel kuerzel und eine für den Wert name. Beide mit einem Komma getrennt und zwischen runden Klammen eingeschlossen. Wie bei Sets ist der Inhalt eines Dictionarys von Natur aus ungeordnet.

Du kannst auch For-In-Schleifen für numerische Bereiche verwenden. Betrachte das folgende Beispiel:

for index in 1...10 {
    print("\(index) mal 2 ist \(index * 2)")
}

 

Der Ausdruck 1...10 bezeichnet die Menge der ganzen Zahlen von 1 bis 10, oder anders ausgedrückt einen Zahlenbereich. Die Laufvariable index nimmt somit genau diese Werte an. Mit Hilfe des Zahlenbereichs 1...10 wird bestimmt, wie oft die Schleife durchlaufen werden soll, nämlich 10 Mal. Der Zahlenbereich selbst kann mithilfe von Variablen oder Konstanten definiert werden. Siehe folgendes Beispiel:

let anfang = 3
let ende   = 7

for index in anfang...ende {
    print("\(index) mal 2 ist \(index * 2)")
}

 

Hier wird die Schleife genau 5 mal durchlaufen. Dabei nimmt die Laufvariable index nacheinander die Werte 3, 4, 5, 6 und 7 an. Es ist auch möglich den Bereich mithilfe des Operators ..< festzulegen. So bedeutet folgender Ausdruck 1..<10, dass die Zahlen 1 bis einschließlich 9 durchlaufen werden, nicht aber die 10.

Manchmal möchte man einen Block SourceCode mehrmals durchlaufen, jedoch ist eine Laufvariable nicht erwünscht bzw. erforderlich. In solchen Fällen kannst Du explizit auf die Laufvariable verzichten, indem Du _ schreibst. Ein Beispiel kann dann folgendermaßen aussehen.

var ergenis = 1
for _ in 1...10 {
    ergenis += ergenis
}

print("2 hoch 10 = \(ergenis)")

 


Ein Array ist eine bestimmte Datenstruktur, die zu den so genannten Collections gehört. Eine For-In-Schleife (Flow Control) ist ein Sprachkonstrukt, das uns erlaubt den gleiche Source Code mehrmals hintereinander auszuführen. Lies dir die Passagen durch, dann wird alles klarer.

func getOperatorIndex(_ expression : String) -> String.Index? {
    let validOperators : [Character] = ["+", "-", "*", "/"]
    
    for anOperator in validOperators {
        if let index  = expression.index(of: anOperator) {
            return index
        }
    }
    
    return nil
}

 

Aber was ist hier der eigentliche Vorteil?

    1. Nun, die Funktion wird nicht wachsen, wenn weitere Operatoren hinzugefügt werden. Nur das Array validOperators muss erweitert werden. Es ist also eine minimale Änderung notwendig.
    2. Es ist jetzt sogar möglich die Liste der erlaubten Operatoren außerhalb der Funktion zu definieren. Dadurch kann die Liste der Operatoren an anderen Stellen des Source Codes wiederverwendet werden.
    3. Die Funktion reduziert sich auf die Tätigkeit, den Index eines beliebigen Operators zu finden, ohne zu wissen, um welchen Operator es sich handelt. Die Funktion ist dadurch einfacher geworden.

Ok, die Definition der erlaubten Operatoren kann also auch außerhalb der Funktion liegen. Dann machen wir das doch:

let validOperators : [Character] = ["+", "-", "*", "/"]

func getOperatorIndex(_ expression : String) -> String.Index? {    
    for anOperator in validOperators {
        if let index  = expression.index(of: anOperator) {
            return index
        }
    }
    
    return nil
}

 

Als nächstes lass uns die Funktion compute(expression : String) leicht verändern:

func compute(expression : String) -> Double? {
    if let operatorIndex = getOperatorIndex(expression) {
        let operatorSymbol = expression[operatorIndex]
        let leftUntrimmed  = expression[..<operatorIndex]
        let leftStr        = leftUntrimmed.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
        let left           = Double(leftStr)!
        let rightUntrimmed = expression[expression.index(after: operatorIndex)..<expression.endIndex]
        let rightStr       = rightUntrimmed.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
        let right          = Double(rightStr)!
        
        switch operatorSymbol {
            case "+": return add(value1: left , value2: right)
            case "-": return subtract(value1: left , value2: right)
            case "*": return multiply(value1: left , value2: right)
            case "/": return divide(value1: left , value2: right)
            default:  return nil
        }
    }
    
    return nil
}

 

Wie Du sicherlich schnell erkennen wirst, habe ich die If-Anweisungen durch eine Switch-Anweisung ersetzt. Diese Variante bringt hier nicht wirklich viele Vorteile. Aber eines tut eine Switch-Anweisung immer: Sie zwingt uns über die erlaubten Alternativen nachzudenken. Und wir haben immer einen Fall für die nicht erlaubten Zeichen. Hier ist es der Fall default.

Switch-Anweisung

Eine Switch-Anweisung ähnelt in ihrer Funktion hintereinander ausgeführten If-Anweisungen. Betrachte folgenden Sourcecode:

let dayOfWeek = 2

if (dayOfWeek == 1) {
    print("Montag")
} else if (dayOfWeek == 2) {
    print("Dienstag")
} else if (dayOfWeek == 3) {
    print("Mittwoch")
} else if (dayOfWeek == 4) {
    print("Donnerstag")
} else if (dayOfWeek == 5) {
    print("Freitag")
} else if (dayOfWeek == 6) {
    print("Samstag")
} else if (dayOfWeek == 7) {
    print("Sonntag")
} else {
    print("Die Woche hat nur 7 Tage.")
}

 

Der Wert der Variablen dayOfWeek wird überprüft und gegebenenfalls ein Text ausgegeben. Beachte bitte, dass sobald eine Bedingung wahr ist, die restlichen If-Abfragen nicht mehr ausgeführt werden müssen, da sie sich jeweils hinter dem Else-Zweig befinden.

Eine Switch-Anweisung bietet eine Alternative zu der oben gezeigten If-Else-Kaskade. Betrachte dazu folgenden Sourcecode:

switch dayOfWeek {
case 1 : print("Montag")
case 2 : print("Dienstag")
case 3 : print("Mittwoch")
case 4 : print("Donnerstag")
case 5 : print("Freitag")
case 6 : print("Samstag")
case 7 : print("Sonntag")
default: print("Die Woche hat nur 7 Tage.")
}

 

Eine Switch-Anweisung beginnt mit dem Schlüsselwort switch. Dann folgt ein Wert, der im Folgendem überprüft wird. Im Beispiel ist es der Wert der Variablen dayOfWeek. Nach dem Wert folgt ein Sourcecode-Block, der in geschweiften Klammern {…} eingeschlossen ist. Innerhalb des Sourcecode-Blocks werden alle möglichen Werte, die die betrachtete Variable dayOfWeek haben kann, mithilfe des Schlüsselworts case unterschieden.

Angenommen dayOfWeek hat den Wert 3, dann würde case 3 greifen, und der Sourcecode zwischen dem Doppelpunkt hinter case 3: und dem nächsten case würde ausgeführt. In diesem Fall also print(“Mittwoch”). Nach Ausführung des Case-Sourcecodes erfolgt die Abarbeitung des Programms hinter der Switch-Anweisung weiter.

Und was passiert, wenn die Variable dayOfWeek einen Wert einnimmt, für den kein Fall (case) definiert ist? Nun, dafür ist der letzte Fall zuständig, der mit dem Schlüsselwort default eingeleitet wird. Es ist somit gewährleistet, dass für jeden Wert ein Fall definiert ist. Merke Dir eins: In Switch-Anweisungen muss jeder mögliche Falls abgefangen werden. Das ist bei If-Anweisungen nicht der Fall.

 


Ich denke, das waren fürs Erste genug Verbesserungsvorschläge und dass du mit meinen Tutorials mittlerweile einen guten Einblick in die Programmiersprache Swift bekommen hast. Natürlich gibt es noch viel zu lernen, aber mit dem bisher Besprochenen kannst du schon sehr weit kommen. Im nächsten Teil meiner Tutorial-Reihe werde ich dich in das objekt-orientierte Paradigma einführen. Das hört sich jetzt vielleicht etwas theoretisch an – ich verspreche dir aber, dass es sehr interessant wird. Ok, dann bis zum nächsten Beitrag.

    
Ü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.