Bisher haben Sie Variablen kennengelernt, die »einfache« Daten wie beispielsweise eine Ganzzahl oder eine Fließkommazahl enthalten. Und auch Objekte, die komplizierte Daten und sogar Funktionalität enthalten. Aber was machen Sie, wenn Sie noch gar nicht wissen, wie viele Informationen ein Benutzer Ihres Programms speichern möchte?
Denken Sie mal an das Musikprogramm auf dem iPhone oder an iTunes auf dem Mac. Manche Benutzer haben vielleicht nur ein Dutzend Lieder, andere haben jedoch Tausende und diese sind in Dutzenden zum Teil verschachtelter Playlisten enthalten.
Die Antwort lautet: Container-Klassen. Ein anderer gängiger Begriff lautet Datenfelder. Ich bevorzuge für diese Klassen in diesem Kapitel weitgehend die englischsprachigen Begriffe, denn diese werden von Programmierern im Swift-Umfeld sofort verstanden.
Eine Container-Klasse ist ein Objekt, das mehrere andere Daten speichert und das auch in der Lage ist, sie zu entfernen oder hinzuzufügen. Container-Klassen werden von Apple zum Beispiel für die Listen von Musikdateien und die Playlisten verwendet oder auch für die Sammlung von Adressen, die Sie gespeichert haben. Sie sind sehr leistungsfähige und flexible Objekte und in diesem Kapitel lernen Sie, wie Sie diese Container benutzen und auch, wie Sie diese speichern und wieder laden.
Ein Array ist ein Container, der Daten in einer festen Reihenfolge speichert. Es ist wie ein Karteikasten, dessen Karten durchlaufend nummeriert sind. Auf jeder dieser Karteikarten findet sich ein einzelner Eintrag. In Swift speichern Sie nur ein und denselben Datentyp in den Einträgen. Welcher das ist, geben Sie bei der Erzeugung an. Ich empfehle Ihnen, zum Ausprobieren einen neuen Playground mit Namen »Container« zu erstellen. Dann probieren Sie die verschiedenen Arrays und Methoden direkt aus.
Ein Array schreiben Sie mit eckigen Klammern []
, in denen die einzelnen Einträgen getrennt von einem Komma geschrieben sind. Grundsätzlich ist Swift dabei clever: Je nachdem, was für Daten in den eckigen Klammern stehen, weiß Swift auch gleich, was für einen Container Sie denn erzeugen. Beispielsweise ist
let zahlen = [3, 5, 2, 1]
ein Array bestehend aus Ganzzahlen vom Typ Int
.
Abbildung 10.1 Beispiel für ein Array mit vier Zahlen vom Typ »Int«
Abbildung 10.1 zeigt das Array grafisch. Beachten Sie dabei, dass die Zählung mit der Position 0 beginnt und in diesem Beispiel mit vier Objekten bei drei endet. Ja, genau, die virtuellen Karteikarten sind immer mit einem Index nummeriert, der bei null anfängt. Das bedeutet, dass Sie das n
-te Objekt immer an der Stelle mit Index n--1
finden.
Sie greifen auf ein Element zu, indem Sie den Index in eckigen Klammern schreiben, also beispielsweise:
let eineZahl = zahlen[1]
kopiert das zweite Element (mit Index 1) in die Variable eineZahl
vergewissern Sie sich, dass sie wirklich 5 ist!
Direkt ist auch das letzte Element zu erreichen, und zwar mit der Eigenschaft last
:
let letzteZahl = zahlen.last
In diesem Fall ist letzteZahl
gleich 1.
Ebenfalls sehr bequem ist die Möglichkeit, eine Schleife über alle Elemente zu schreiben. Dabei benutzen Sie die for
-Schleifen:
for zahl in zahlen {
// zahl können Sie nun als Konstante benutzen.
print("Eintrag: \(zahl)")
}
Innerhalb des Blocks der for
-Schleife ist zahl
nun eine Konstante vom Typ Int
.
Alternativ sind natürlich »normale« Schleifen über den Index möglich. Hierfür benötigen Sie die Eigenschaft count
, die die Gesamtzahl der Elemente enthält. Die Schleife über alle Elemente lautet dann:
for var meinIndex=0; meinIndex<zahlen.count; meinIndex += 1 {
// meinIndex ist eine normale Int--Variable.
print("Eintrag: \(zahlen[meinIndex])")
}
Ich persönlich bevorzuge da die erste Variante, sofern es möglich ist.
Beachten Sie, dass Sie das Array zahlen
durch let
als Konstante definiert haben. Dadurch dürfen Sie es nach der Erzeugung nicht mehr ändern, also keine Elemente hinzufügen oder entfernen. Um das Array zu ändern, weisen Sie es einer Variablen zu. Dabei wird das Array kopiert und die Kopie darf verändert werden:
var meineZahlen = zahlen
Beispielsweise lässt sich ein einzelnes Element an das Array anzufügen, indem Sie die Methode append
benutzen:
meineZahlen.append(4)
Nun enthält meineZahlen
die Einträge [3, 5, 2, 1, 4]
, jedoch zahlen
ist unverändert.
Dadurch, dass meineZahlen
bei der Erzeugung nur Ganzzahlen vom Typ Int
speichern musste, erzeugt die folgende Zeile einen Fehler:
meineZahlen.append(4.5)
Dieser Aufruf versucht, eine Fließkommazahl an meineZahlen
anzuhängen, obwohl meineZahlen
nur Int
speichern darf. Sie könnten dies ändern, indem Sie in der ersten Zeile bei der Erzeugung schreiben:
let zahlen = [3.0, 5, 2, 1]
Indem Sie die 3
in eine Fließkommazahl 3.0
ändern, nimmt Swift an, dass Sie ein Array von Fließkommazahlen erzeugen wollen und konvertiert alle anderen Zahlen des Arrays von sich aus in Fließkommazahlen das Array wird zu einem Arrays, das ausschließlich aus Fließkommazahlen besteht. Sie können auch explizit den Datentyp angeben, den das Array haben soll: Bei der Erzeugung schreiben Sie Array
, gefolgt von dem gewünschten Datentyp zwischen einem »kleiner als«-Zeichen <
und einem »größer als«-Zeichen >
. So bezeichnet Array<Int>
ein Array aus Zahlen vom Typ Int
das ist genau der Datentyp, den Swift zahlen
und meineZahlen
automatisch gegeben hat. Wenn Sie aber explizit schreiben:
let zahlen:Array<Double> = [3, 5, 2, 1]
so erzeugen Sie das gewünschte Array
aus Double
-Zahlen. Wenn Sie dann später meineZahlen.append(4.5)
schreiben, funktioniert die Anweisung einwandfrei.
[Int]
ist ein Array
aus Int
. Diese Schreibweise ist ein bisschen klarer als Array<Int>
.Das Löschen einzelner Elemente erledigen Sie mit der Methode removeAtIndex
, die als Parameter den Index bekommt, der gelöscht werden soll. Beispielsweise löscht
meineZahlen.removeAtIndex(1)
den zweiten Eintrag des Arrays. Alle späteren Einträge rücken dann auf und die gesamte Zahl der Elemente reduziert sich um eins.
Schließlich lassen sich einzelne Elemente ersetzen, indem Sie einfach einem bestimmten Element einen neuen Wert zuweisen. Beispielsweise ersetzen Sie den ersten Eintrag des Arrays durch den neuen Wert 101:
meineZahlen[0] = 101
Für den Rest dieses Buches benötigen Sie nur die hier aufgeführten Methoden. Für eine vollständige Übersicht über alle Möglichkeiten, die ein Array Ihnen bietet, empfehle ich Ihnen die Dokumentation von Apple.
In Abschnitt 5.1 haben Sie die Regeln von FizzBuzz kennengelernt. Und vielleicht auch lieben oder hassen, aber auf jeden Fall programmieren gelernt. Die Lösung aus Listing 5.1 hat zwar funktioniert, war aber auch recht unübersichtlich und fehleranfällig Sie mussten mehrere if
-Abfragen verschachteln und durften bei der Reihenfolge keinen Fehler machen!
Nun stelle ich Ihnen eine einfachere und elegantere Alternative vor, die ohne abstruse und unschöne Schachtelung von geschweiften Klammern, alternative Programmflüsse und sonstigen Krimskrams auskommt. Sie setzt Vorteile des Array
ein!
Die neue Strategie: Sie benutzen ein Array<String>
mit 100 Einträgen. Jeder Eintrag ist dabei entweder eine Zahl (als String
) oder einer der Strings »Fizz«, »Buzz« oder »FizzBuzz«. Beachten Sie dabei aber, dass der Index eines Array
bei 0 anfängt. Das heißt, der Wert des ersten Eintrags (bei Index 0) ist »1« und unterscheidet sich daher vom Index. Dies ist zwar nicht ungewöhnlich, aber manchmal vergessen Programmierer solche Kleinigkeiten und dies führt dann zu den Off-by-one-Fehlern, die ich bereits in Abschnitt 6.1.1 erwähnt hatte.
Dann machen Sie vier Schleifen:
Anschließend enthält das Array die korrekten Antworten für die ersten 100 FizzBuzz-Einträge. Einen Ausschnitt aus dem Array zeigt Abbildung 10.2.
Abbildung 10.2 Ausschnitt aus dem »Array¡String¿«, das die korrekten Antworten enthält
Die Implementierung der vier Schleifen ist in Listing 10.1 zu sehen. Die Funktion erzeugeFizzBuzzArray
liefert das gesucht Array<String>
zurück und die anschließende Schleife gibt alle Einträge des Arrays aus.
/// Berechne das FizzBuzz--Array für 100 Einträge neu und gib es
/// zurück.
func erzeugeFizzBuzzArray() --> Array<String> {
// Das anfängliche leere Array.
var alleEintraege = Array<String>()
// Zunächst wird das Array nur mit den Zeilennummern befüllt.
for var wert=1; wert<=100; wert+=1 {
alleEintraege.append("\(wert)")
}
// Dann wird jede dritte Zeile durch "Fizz" ersetzt.
for var wert=1; wert<=100/3; wert+=1 {
alleEintraege[wert*3--1] = "Fizz"
}
// Im nächsten Schritt wird jede fünfte Zeile durch "Buzz" ersetzt.
for var wert=1; wert<=100/5; wert+=1 {
alleEintraege[wert*5--1] = "Buzz"
}
// Schließlich wird jede fünfzehnte Zeile durch "FizzBuzz" ersetzt.
for var wert=1; wert<=100/15; wert+=1 {
alleEintraege[wert*15--1] = "FizzBuzz"
}
// Gib das gesamte Array zurück.
return alleEintraege
}
// Gib alle Zeilen, die erzeugeFizzBuzzArray liefert, aus.
for zeile in erzeugeFizzBuzzArray() {
print("\(zeile)")
}
Listing 10.1 Die Implementierung von FizzBuzz mithilfe eines Arrays ohne verschachtelte »if«-Blöcke
Vergewissern Sie sich, dass die Ausgabe von Listing 10.1 in Ihrem Playground korrekt ist.
Die hier vorgestellte Methode hat auch Nachteile: Sie speichern die eigentliche Ausgabe in einem großen Array ab. Wenn Sie nicht nur die ersten 100 Zahlen, sondern ein paar Tausend ausrechnen würden, so würden Sie sehr viel Speicherplatz verbrauchen. Außerdem erzeugen Sie mehrere Schleifendurchläufe, was ein bisschen langsamer als die ursprüngliche Lösung ist.
Neben einem Array
bietet Swift noch eine weitere, sehr mächtige Klasse an, die andere Objekte speichern und zurückgeben kann. Diese nennt sich Dictionary
. Die wörtliche Übersetzung ins Deutsche wäre in etwa »Wörterbuch«. Allerdings ist das keine besonders einleuchtende Beschreibung dafür, worum es sich dabei handelt. In akademischen Texten findet sich auch die Übersetzung assoziatives Datenfeld, allerdings dürften nur die wenigsten praktischen Programmierer diesen Begriff täglich gebrauchen. In anderen Programmiersprachen gibt es die Begriffe »Hash-Tabelle« oder auch »map«.
Ein Dictionary
speichert die Objekte nicht in einer festen Reihenfolge ab, sondern sie werden wie Karteikarten mit einer Beschriftung abgelegt. Die Beschriftung ist in der Regeln ein String
. Die Beschriftung bezeichnet man auch als Schlüssel und den eigentlichen Inhalt als Wert.
Ein Dictionary erzeugen Sie, indem Sie die gewünschten Paare des Schlüssels und seines Wertes durch Kommata getrennt in eckige Klammern []
schreiben das ähnelt einem Array. Schlüssel und Wert werden dabei jeweils durch einen Doppelpunkt getrennt. Ein Dictionary bestehend aus den Schulnoten von »Sehr gut« bis »Ausreichend« wird beispielsweise folgendermaßen erzeugt:
var schulNoten = ["Sehr gut": 1, "Gut": 2,
"Befriedigend": 3, "Ausreichend": 4]
Abbildung 10.3 zeigt dieses Dictionary mit vier Einträgen. Beachten Sie, dass die Einträge nicht mehr der Reihe nach angeordnet sind, sondern an ihrem Reiter in der linken oberen Ecke mit ihrem Schlüssel beschriftet.
Abbildung 10.3 Beispiel für ein Dictionary mit vier Einträgen
Im Grunde genommen gleicht ein Array einem Dictionary, das Zahlen als Beschriftung der Karteikarten verwendet allerdings mit teilweise anderer Schreibweise. Allerdings arbeiten beide intern doch ein wenig anders und Sie müssen sich überlegen, was in einer bestimmten Situation die einfachste und natürlichste Lösung ist.
Genauso wie bei Arrays gilt: Wenn Sie ein Dictionary mit let
erzeugen, so dürfen Sie es nachträglich nicht mehr verändern, also keine Einträge hinzufügen, ändern oder entfernen. Bei einer Erzeugung mit var
hingegen ist das erlaubt.
Auf die Einträge greifen Sie zu, indem Sie den Schlüssel in eckige Klammern schreiben. Dies liefert den Wert des Dictionaries, also beispielsweise
let sehrGut = schulNoten["Sehr gut"]
Nun ist sehrGut
ein Int
mit Wert 1. Wenn Sie weitere Schulnoten hinzufügen oder einen bestehenden Eintrag ändern wollen, so weisen Sie einem Schlüssel einen neuen Wert zu:
// Erzeugt einen neuen Eintrag.
schulNoten["Mangelhaft"] = 55
// Ändert den gerade hinzugefügten Eintrag.
schulNoten["Mangelhaft"] = 5
Genauso wie bei einem Array »errät« Swift die Datentypen, die in einem Dictionary enthalten sind, wenn Sie es erzeugen. Sie können die beiden Datentyp explizit angeben, indem Sie schreiben:
var meineNoten:Dictionary<String, Int>
Dies erzeugt eine Variable, die ein Dictionary
bestehend aus String
und Int
enthält. Dies ist in diesem Fall der gleiche Datentyp wie das Dictionary schulNoten
von oben. Auch hier gibt es wieder die alternative Schreibweise
var meineNoten:[String: Int]
Sie können diese Schreibweise vorziehen, wenn sie Ihnen mehr zusagt.
Sie können alle Einträge eines Dictionaries auf verschiedenen Wegen bekommen und benutzen je nachdem, was Sie gerade brauchen. Die folgenden Methoden, Schlüssel und Werte abzufragen, sind besonders nützlich:
keys
und schreiben:// Gib alle Schlüssel in schulNoten aus.
for schluessel in schulNoten.keys {
print("\(schluessel)")
}
keys
die Eigenschaft values
abfragen:// Gib alle Werte in schulNoten aus.
for wert in schulNoten.values {
print("\(wert)")
}
// Gib alle Schlüssel/Werte Paare in schulNoten aus.
for (schluessel, wert) in schulNoten {
print("\(schluessel) entspricht als Zahl \(wert)")
}
Beachten Sie, dass die Ausgabe in keinem dieser Fälle sortiert ist. Ein Dictionary speichert »seine« Schlüssel/Werte Paare ohne besondere Sortierung ab. Um eine bestimmte Sortierung zu bekommen, müssen Sie diese gesondert anfordern. Darum geht es im folgenden Abschnitt.
Datencontainer in Objective-C haben nur untypisierte Arrays angeboten. Dadurch konnten Sie alle möglichen Objekte (und auch nur Objekte) in einem Array speichern. Dies konnte zu Fehlern führen, denn Objective-C hat Sie nicht gewarnt, wenn Sie mal versehentlich ein »falsches« Objekte abgelegt haben.
Swift bietet Ihnen hier die angenehme Wahl zwischen beiden Möglichkeiten: Sie entscheiden sich für einen bestimmten Datentyp und Swift hilft Ihnen dabei, Fehler zu vermeiden, indem es kontrolliert, ob Sie immer den richtigen Datentyp speichern. Alternativ können Sie ebenfalls beliebige Objekte speichern, indem Sie als Datentyp AnyObject
angeben. Dies ist in den meisten Fällen nicht notwendig, aber es gibt Betriebssystemfunktionen und -methoden, die solche Daten zurückliefern.
Ein untypisiertes Array müssen Sie immer selbst erzeugen, indem Sie schreiben:
var kannAllesSpeichern:Array<AnyObject> = []
Nun können Sie sprichwörtlich beliebige Daten in kannAllesSpeichern
packen:
// Speichere Zahlen ab.
kannAllesSpeichern.append(101)
kannAllesSpeichern.append(3.14159)
// Speichere einen Bool ab.
kannAllesSpeichern.append(true)
// Speichere einen String ab.
kannAllesSpeichern.append("Haha!")
// Speichere ein Unternehmer--Objekt ab.
kannAllesSpeichern.append(Unternehmer())
// Am Ende füge noch einen weiteren String hinzu.
kannAllesSpeichern.append("Viel Spaß!")
Das sieht zunächst einmal gut aus, aber wie kommen Sie denn an die Daten wieder ran? Welcher Datentyp ist denn wo enthalten? Dafür müssen Sie die Daten mit Hilfe von as
wieder umwandeln. Es gibt zwei Möglichkeiten:
as?
, also as
mit einem angehängten Fragezeichen. Dies erzeugt einen optionalen Datentyp, der entweder nil
ist wenn Sie den »falschen« Datentyp genannt haben oder den gewünschten Wert enthält. Für optionale Datentypen siehe Abschnitt 9.2.1.as!
, also as
mit einem angehängten Ausrufungszeichen. Dies erzeugt einen nicht-optionalen Datentyp. Oder einen Fehler, wenn Sie den »falschen« Datentyp erwischt haben.Im obigen Beispiel enthält das Array kannAllesSpeichern
völlig verschiedene Werte. Wenn Sie eine Schleife über das Array laufen lassen, ist es sinnvoll, die zweite Variante zu verwenden. Um sich alle Strings ausgeben zu lassen, funktioniert die folgende Schleife:
// Schleife über das gesamte Array.
for eintrag in kannAllesSpeichern {
// Gib alle Einträge, die Strings sind, aus.
if let zeichenkette = eintrag as? String {
print("Ein String: \(zeichenkette)")
}
}
Vergewissen Sie sich, dass die Ausgabe wirklich
Ein String: Haha!
Ein String: Viel Spaß!
lautet. Alternativ schreiben Sie, wenn Sie das vierte Element als String haben wollen:
let textHaha = kannAllesSpeichern[3] as! String
Dies funktioniert aber auch wirklich nur dann, wenn dieses Element auch wirklich als String
gespeichert wurde oder zumindest direkt konvertiert werden kann!
Es gibt übrigens noch eine dritte Variante von as
: Wenn sich Datentypen immer konvertieren lassen und keine Überprüfung notwendig ist (beispielsweise zwischen Int
und Double
), so verwenden Sie as
, also beispielsweise:
var meineZahl = 101 as Double
Diese Schreibweise ist äquivalent zu Double(101)
.
Eine sehr wichtige und häufig benutzte Funktion ist das Speichern und Laden von Daten auf der Festplatte Ihres Rechners. Oder auch auf der SSD Ihres iPhones oder iPads. Natürlich ist das nicht immer sinnvoll – vielleicht wollen Sie ein wichtiges Benutzerpasswort bei jedem Programmstart neu abfragen, anstatt es zu speichern. Aber wenn Ihre Benutzer in der App ein paar Tausend Adressen speichern, könnte es einen gewissen Komfortgewinn darstellen, wenn Ihre Anwender diese nicht bei jedem Start neu eingeben müssten.
Deswegen zeige ich Ihnen im Folgenden, wie Sie Arrays, Dictionaries und Strings speichern und wieder laden können. Das funktioniert leider nur für die eingebauten Datentypen wenn Sie eigene Objekte direkt speichern wollen, ist es ein wenig komplizierter.
Bevor Sie Objekte speichern und laden, müssen Sie aber erst noch einen Dateinamen wählen, unter dem die Daten gespeichert und geladen werden. Zu diesem Dateinamen gehört einerseits das Verzeichnis, andererseits aber auch der Dateiname selbst.
Das Verzeichnis ist dabei leider nicht ganz so einfach es enthält unter MacOS X den Benutzernamen und unter iOS eine absolut kryptische App-ID. Ach ja, und was ist mit der Übersetzung des DOKUMENTE-Ordners für koreanische Anwender? Diesen richtigen Namen zu konstruieren, ist daher alles andere als einfach. Glücklicherweise bietet Apple hier eine Hilfestellung und liefert eine Methode, die sich genau um diese Dinge kümmert. Listing 10.2 zeigt, wie Sie das DOKUMENTE-Verzeichnis für den aktuellen Benutzer (unter MacOS X) beziehungsweise für die aktuelle App (unter iOS) erhalten. Sie funktioniert universell für alle Apple-Betriebssysteme, -versionen und Sprachen.
// Hole das "Dokumente" Verzeichnis.
let dokumentenVerzeichnis = NSSearchPathForDirectoriesInDomains(
NSSearchPathDirectory.DocumentDirectory,
NSSearchPathDomainMask.UserDomainMask,
true).last!
Listing 10.2 Auffinden des »Dokumente«-Verzeichnisses eines Benutzers auf allen Apple-Systemen
In Listing 10.2 liefert die Funktion NSSearchPathForDirectoriesInDomains
ein Array<String>
zurück, das alle Verzeichnisse enthält, auf die die Suchkriterien zutreffen. Das erste Argument NSSearchPathDirectory.DocumentDirectory
ist eine Konstante, die als Suchkriterium das DOKUMENTE-Verzeichnis liefert, und das zweite Argument NSSearchPathDomainMask.UserDomainMask
besagt, dass nur das Verzeichnis für den aktuellen Benutzer zurückgeliefert werden soll. Aus diesem Grund liefert die Funktion auch ein Array
zurück, denn je nachdem, was man anfragt, bekommt man unter Umständen mehrere Verzeichnisse zurückgeliefert. Das dritte Argument sollte auf true
gesetzt sein und besagt, das der Pfad nicht verkürzt werden soll.
Die Eigenschaft last
des Array
liefert das letzte Objekt. Da die Funktion mit den gegebenen Argumenten immer genau ein Verzeichnis zurückliefert, ist dies auch genau das gesuchte Verzeichnis. Zuletzt stellt das Ausrufungszeichen !
sicher, dass auch wirklich ein echter Wert und nicht nur ein optionaler String
geliefert wird ein optionaler Wert ist nämlich sonst die Voreinstellung.
Um jetzt den vollständigen Dateinamen einschließlich Verzeichnis zu bekommen, müssen Sie noch die beiden Dinge kombinieren. Wie Sie dies mit String
-Objekten tun, wissen Sie bereits. Allerdings bietet es sich an, zwischen Verzeichnis und Dateinamen noch einen weiteren Verzeichnistrenner /
einzufügen, damit auch wirklich das Verzeichnis stimmt. Somit lautet der vollständige Dateiname:
// Der Name der Datei.
let dateiName = "MeineDaten.dat"
// Der vollständige Name mit Verzeichnispfad und Datei.
let vollerName = dokumentenVerzeichnis + "/" + dateiName
Schließlich brauchen Sie noch die Methoden, um die Daten in die Datei zu speichern und von dort auch wieder zu laden. Leider bieten gerade die Klassen Array
und Dictionary
keine bequeme Methode an, um Daten zu speichern und zu laden. Die Cocoa-Objekte NSArray
und NSDictionary
hingegen tun das schon diese Objekte sind allen Objective-C Programmierern bestens bekannt, denn es sind die dort üblichen Klassen für Arrays und Dictionaries. Ich empfehle Ihnen, mal auf die Hilfeseite von NSArray
zu schauen und dort die Methoden zu suchen, die Daten speichern und laden. Der wichtigste Unterschied zu einem Swift-Array und -Dictionary besteht darin, dass die Cocoa-Objekte grundsätzlich untypisiert sind, das heißt sie entsprechen immer einen Container vom Datentyp AnyObject
, vergleiche Abschnitt 10.4.
Die Methoden sind writeToFile
zum Speichern und die Initialisierungsmethode mit Parameter contentsOfFile
. Das bedeutet, dass Sie problemslos Daten speichern und laden könnten, wenn Sie ein NSArray
anstelle eines Array
hätten. Glücklicherweise kann Swift einfach zwischen beiden Klassen hin- und herwechseln. Sie schreiben
let cocoaArray:NSArray = [1, 2, 3]
Dadurch wird das ursprüngliche Array in ein NSArray
umgewandelt. Das Speichern geht nun ganz einfach:
cocoaArray.writeToFile(vollerName, atomically:true)
Der Parameter atomically
besagt, dass die Operation nicht unterbrochen werden kann das heißt, die Datei ist entweder vollständig da oder gar nicht. Das macht in der Praxis keinen großen Unterschied, es sei denn, Sie schalten Ihren Computer genau in dem Moment aus, in dem die Datei gespeichert wird. Ich empfehle Ihnen, immer true
zu verwenden.
Das Laden eines Arrays funktioniert so:
var geladen = NSArray(contentsOfFile:vollerName) as! Array<Int>
Dabei ist vollerName
wieder der volle Dateiname einschließlich Verzeichnispfad. Beachten Sie, dass die dazugehörige Datei auch existieren und ein entsprechendes NSArray
gespeichert haben muss. Das NSArray
wird geladen und durch as! Array<Int>
automatisch in ein Swift-Array vom Datentyp Int
gewandelt. Beachten Sie, dass dieser Schritt schiefgehen kann, falls das geladene NSArray
Werte enthält, die keine Zahlen sind! Wenn Sie nicht sicher sein können, was im geladenen Array »drin« ist, so gehen Sie auf Nummer sicher mit:
var geladen = NSArray(contentsOfFile:vollerName) as! Array<AnyObject>
In diesem Fall müssen Sie noch die »falschen« Einträge herausfiltern. Das machen Sie in einer Übungsaufgabe zu diesem Kapitel.
Ich zeige Ihnen jetzt ein ganz konkretes Beispiel für eine Strategie, die man oft verwendet, um Arbeit zu sparen. Das Beispiel ist die Berechnung des Fizzbuzz-Arrays und das Speichern desselben in einer Datei. Wenn die Datei bereits existiert, dann lädt das Programm die Datei und zeigt das Array an. Wenn die Datei noch nicht existiert, dann wird das Array berechnet, gespeichert und dann angezeigt. Auf diesem Wege ersparen Sie Ihrem Mac eine Neuberechnung, wenn diese nicht unbedingt erforderlich ist. Dies ist in diesem Fall nicht unbedingt ein Problem. Aber wenn die Berechnung sehr aufwendig wäre, ist diese Strategie sehr gängig und nennt sich Memoisation. Darunter versteht man das Zwischenspeichern komplizierter oder aufwendiger Ergebnisse in anderen Variablen oder wie in diesem Fall in einer eigenen Datei.
Eine Kleinigkeit ist aber noch nötig, bevor Sie das Programm schreiben können. Und zwar müssen Sie noch wissen, wie Sie denn feststellen, ob die Datei bereits existiert.
Dafür bietet Apple aber bereits einen Weg, und zwar mit der Klasse NSFileManager
. Hier müssen Sie zwei Methodenaufrufe kombinieren, und zwar zunächst einen Aufruf zur Methode defaultManager
und danach den Aufruf fileExistsAtPath
. Zunächst liefert defaultManager
ein Objekt vom Typ NSFileManager
zurück, also zum Beispiel
let derDateiManager = NSFileManager.defaultManager()
Hier wird ein fertiges Objekt zurückgeliefert, das Sie anschließend benutzen. Um die Existenz einer Datei namens vollerName
abzufragen, benutzen Sie dann die Methode fileExistsAtPath
, die einen String
bekommt und einen Bool
zurückliefert:
let dateiExistiert = derDateiManager.fileExistsAtPath(vollerName)
Im Folgenden empfehle ich Ihnen, ein Kommandozeilenprojekt namens »FizzBuzzMemo« anzulegen. Der DOKUMENTE-Ordner eines Playground ist nämlich ein anderer als der »normale« Ordner unter MacOS X. Aus diesem Grunde ist ein Kommandozeilenprojekt für die folgenden Schritte einfacher nachzuvollziehen.
defaultManager
immer das gleiche Objekt zurück, selbst wenn Sie es an völlig unterschiedlichen Stellen im Programm aufrufen. Ein solches Objekt nennt man auch Singleton. Ein Singleton existiert nur ein einziges Mal und Sie verwenden es im gesamten Programm.Nun kopieren Sie den Programmtext aus Listing 10.1 in die Datei main.swift
und drücken zunächst +
, um sich zu vergewissern, dass das Programm so funktioniert, wie es soll.
Nun benötigen Sie eine Funktion, die anstelle der existierenden Funktion erzeugeFizzBuzzArray
das Array nicht einfach nur erzeugt. Stattdessen soll die neue Funktion die folgenden Schritte durchführen:
FizzBuzzDaten.dat
existiert, so soll Ihr Inhalt eingelesen werden und anschließend zurückgegeben werden.
erzeugeFizzBuzzArray
erstellt werden. Anschließend wird es zunächst in die Datei geschrieben und dann zurückgegeben.
Listing 10.3 zeigt eine solche Funktion namens holeFizzBuzzArray
. Um das Programm zu testen, empfehle ich Ihnen, die folgenden Schritte durchzuführen:
FizzBuzzDaten.dat
gibt.
main.swift
den Aufruf von erzeugeFizzBuzzArray
durch die neue Funktion holeFizzBuzzArray
.
FizzBuzzDaten.dat
erschienen sein. Sie sollte das FizzBuzz-Array enthalten, allerdings in einem eigenen Format, das Sie nicht ohne weiteres lesen oder bearbeiten können.
/// Lade ein existierendes FizzBuzz--Array aus der Datei
/// "FizzBuzzDaten.dat" oder erzeuge es neu und speichere es ab.
func holeFizzBuzzArray() --> Array<String> {
// Der Name der Datei.
let fizzBuzzDatei = "FizzBuzzDaten.dat"
// Hole das "Dokumente" Verzeichnis.
let dokumentenVerzeichnis = NSSearchPathForDirectoriesInDomains(
NSSearchPathDirectory.DocumentDirectory,
NSSearchPathDomainMask.UserDomainMask,
true).last!
// Der vollständige Name mit Verzeichnispfad und Datei.
let vollerName = dokumentenVerzeichnis + "/" + fizzBuzzDatei
// Prüfe, ob die Datei existiert.
let derDateiManager = NSFileManager.defaultManager()
let dateiExistiert = derDateiManager.fileExistsAtPath(vollerName)
// Definiere das Ergebnis--Array, zunächst ist es leer.
var fizzBuzzArray = Array<String>()
if (dateiExistiert) {
// Lade die Datei.
// Achtung: Keine Fehlerbehandlung!
fizzBuzzArray = NSArray(contentsOfFile:vollerName) as! Array<String>
} else {
// Berechne das Array neu.
fizzBuzzArray = erzeugeFizzBuzzArray()
// Speichere es ab.
let cocoaArray:NSArray = fizzBuzzArray
cocoaArray.writeToFile(vollerName, atomically:true)
}
// Gib das Ergebnis zurück.
return fizzBuzzArray
}
Listing 10.3 Funktion »holeFizzBuzzArray«, die das Array entweder erzeugt und speichert oder lädt
Dies ist das erste Programmbeispiel, das Sie geschrieben haben, das bei mehrmaligen Aufrufen etwas anderes tut als bei einem einzigen Aufruf!
Beachten Sie allerdings, dass dieses Programm nicht überprüft, ob der Inhalt der Datei wirklich »korrekt« ist. Wenn Sie die Datei selbst in Xcode öffnen und mit fehlerhaften Daten befüllen, so ist die Ausgabe des Programms leider nicht mehr korekt. Es gibt Strategien, um zumindest ein versehentliches Fälschen der Daten zu vermeiden nämlich durch so genannten Prüfsummen, siehe als Einleitung den Wikipedia-Artikel http://de.wikipedia.org/wiki/Prüfsumme. Einen Schutz vor willentlichen, boshaften Manipulationen bieten diese allerdings nicht!
Ich gehe auf ein sehr mächtiges Konzept ein, das Swift deutlich besser meistert als beispielsweise Objective-C: die Möglichkeit, Funktionen als Variablen übergeben zu können. Insbesondere in der Kombination mit Arrays und Dictionaries ergeben sich hier viele Möglichkeiten und sehr mächtige Konstruktionen.
Außerdem weise ich kurz auf ein weiteres Thema hin, das für fortgeschrittene Projekte interessant sind: Generics bei der Definition von Klassen und Funktionen.
Stellen Sie sich vor, Sie hätten ein Array von Adressen und wollten alle Namen finden, deren Adresse mit der Postleitzahl »1« beginnen. Anschließend wollen Sie zählen, welche davon auf der »Hauptstraße« wohnen und eine Hausnummer kleiner »100« besitzen. Dann wollen Sie zusätzlich eine Liste, auf die alles bisher Gesagte zutrifft, die jedoch nicht den Namen »Müller« haben.
Das können Sie jetzt schon programmieren: Sie machen eine Schleife über alle Adressen und benutzen dann eine Reihe komplizierter if
-Abfragen, um zwei neue Listen zusammenzubasteln, die dann das gewünschte Ergebnis enthalten.
Und wenn sich die Kriterien ändern? Dann müssen Sie die richtige Schleife finden, die if
-Abfragen auseinanderklamüsern und entsprechend ändern. Funktionale Programmierung hilft gerade bei solchen Aufgaben wesentlich, indem Sie das Problem umformuliert: Sie sagen, was mit einem einzelnen Eintrag passieren soll anstatt selbst Schleifen zu programmieren und Listen zu basteln, geben Sie einfach nur vor, was mit Elementen geschehen soll, wenn diese zu einer neuen Liste hinzugefügt werden sollen.
Das erleichtert Ihre Arbeit doppelt:
Das wesentliche Werkzeug für diesen Zweck ist es, die Bedingungen und Operationen als Funktionen an Arrays zu übergeben. Genau hierbei unterstützt Swift Sie: Sie dürfen Funktionen in Variablen und Konstanten speichern. Sie können diese sogar von anderen Funktionen zurückgeben lassen! In Swift sind dadurch Funktionen ein vollwertiger Datentyp! Diese Art, Funktionen zu verwenden, nennt sich auch funktionale Programmierung.
func
definiert worden ist, sondern sich auf den Datentyp selbst bezieht. Java und Python verwenden dafür speziell das Wort lambda
, aber andere Sprachen wie Swift benutzen dafür eine eigene Schreibweise. Die Möglichkeiten der funktionalen Programmierung sind sehr vielfältig und gehen über den Rahmen dieses Buches weit hinaus. Daher kann ich Ihnen an dieser Stelle nur die wichtigsten Grundlagen vermitteln.
Um anstatt eines Datentyps wie Int
oder Double
eine Funktion zu bezeichnen, müssen Sie die so genannte Signatur der Funktion angeben diesen Begriff hatte ich bereits in Abschnitt 5.1.1 verwendet. Allerdings war es dort ein eher unwichtiges Detail. Wenn Sie Funktionen aber wie einen Datentyp behandeln, so ist die Signatur absolut wichtig. Eine Signatur hat die Form
(Parameter) --> Ergebnis
Dabei ist Parameter
eine durch Komma getrennte Liste von Datentypen und Ergebnis
ein einzelner Datentyp. Die folgende Variable enthält eine Funktion, die einen Int
als Parameter bekommt und einen Int
zurückliefert:
var eineFunktion:(Int) --> Int
Im Moment hat diese Variable noch keinen Wert dies ist eine Ausnahme von der Regel, dass Variablen, die nicht optional sind, immer einen Wert haben müssen.
Wenn Sie nun eine Funktion definieren, so können Sie diese der Variablen eineFunktion
zuweisen. Beispielsweise die Funktion addiereZwei
:
/// Eine Funktion, die zu Ihrem Parameter 2 addiert und zurückgibt.
func addiereZwei(x:Int) --> Int {
return x + 2
}
// Weise diese Funktion der Variablen eineFunktion zu.
eineFunktion = addiereZwei
// Dies liefert als Ergebnis 7.
eineFunktion(5)
Sie können sogar eine Funktion schreiben, die eine andere Funktion zurückgibt. Die folgende Funktion liefert Funktionen zurück, die einen Int
bekommen und einen Int
zurückgeben. Dabei addieren Sie immer einen bestimmten Wert zu ihrem Argument:
/// Liefere eine Funktion zurück, die einen Zuschlag zu Ihrem Argument
/// addiert.
func erzeugeAddierer(zuschlag:Int) --> (Int) --> Int {
// Addiere zuschlag zum Parameter.
func addiere(x:Int) --> Int {
return x + zuschlag
}
// Liefere die Funktion addiere zurück.
return addiere
}
// Erzeuge einen addierer für die Zahl 10 und weise ihn eineFunktion zu.
eineFunktion = erzeugeAddierer(10)
// Dies liefert als Ergebnis 15.
eineFunktion(5)
Weil diese Art von Konstruktionen in der funktionalen Programmierung häufig benötigt werden, gibt es dafür eine Kurzschreibweise:
Parameter in Ausdruck
Dabei sind Parameter
wieder die durch Komma getrennten Datentypen, die als Eingabewerte der Funktion agieren. Ausdruck
ist die Verarbeitung der Parameter.
Damit lässt sich erzeugeAddierer
wie folgt vereinfachen:
func erzeugeAddierer(zuschlag:Int) --> (Int) --> Int {
return { x in return x + zuschlag }
}
Viele eingebaute Klassen und Funktionen in Swift erlauben, solche Arten von Funktionen als Parameter zu übergeben. Hiermit können Sie genau das Problem lösen, von dem ich am Anfang dieses Abschnitts gesprochen habe. Beispielsweise können Sie eine Funktion gleichzeitig auf alle Elemente eines Arrays anwenden, indem Sie die Funktion map
benutzen:
// Definiere ein Array.
var zahlenReihe = [1, 2, 3, 4, 5]
// Quadriere jede Zahl.
var quadrate = zahlenReihe.map({ x in return x*x })
// Dies liefert [1, 4, 9, 16, 25].
quadrate
Wenn sie der letzte Parameter sind, dürfen Funktionen auch als reiner Block ohne darum stehende runde Klammern geschrieben werden, also beispielsweise
var quadrate = zahlenReihe.map { x in return x*x }
Eine fortgeschrittene Anwendung ist die Kombination von Filtern (Einschränkungsregeln) und Reduktionen (wie beispielsweise Aufsummierung). Eine sehr fortgeschrittene Anwendung finden Sie in einer Übungsaufgabe dieses Kapitels.
Eine vollständige Abhandlung über funktionale Programmierung geht über den Rahmen dieses Buches hinaus. Wenn Sie mehr über die Möglichkeiten erfahren möchten, so empfehle ich Ihnen die Dokumentation von Apple zum Stichwort Closures.
Bei der Definition eines Arrays oder eines Dictionaries konnten Sie explizit entscheiden, was für Datentypen enthalten sein durften. Diese Fähigkeit nennt sich Generics. Genauso definieren Sie in Swift auch eigene Klassen und Funktionen. Dafür schreiben Sie bei der Definition einen Variablentyp in spitzen Klammern:
// Gib einen Wert eines beliebigen Typs aus.
func ausgabe<T>(wert:T) {
print("\(wert)")
}
// Beispiel: Ausgabe eines Int--Wertes.
ausgabe(3)
// Ausgabe eines String.
ausgabe("Hallo")
Auf diesem Wege passt sich eine Klasse oder Funktion auf »Ihren« Datentyp an. Wenn Sie mehr darüber erfahren möchten, so empfehle ich Ihnen die Dokumentation von Apple zum Stichwort Generics.
Achtung: Die Aufgaben in diesem Kapitel sind teilweise deutlich schwieriger, als Sie es von vorherigen Kapiteln gewohnt sind! Sie werden möglicherweise wesentlich mehr Zeit als bisher benötigen, um sie richtig zu lösen. Versuchen Sie daher nicht, alle Aufgaben schnell und in einem einzigen Anlauf zu lösen das wird nicht funktionieren. Stattdessen nehmen Sie immer nur kleine Änderungen vor und lassen Sie das Programm immer wieder laufen. Wenn etwas nicht funktioniert, bauen Sie verschiedenen Stellen print
-Anweisungen ein, um zu schauen, ob alle Sachen so funktionieren, wie Sie es gedacht haben.
func summe(zahlen:Array<Int>) --> Int
Int
-Zahleb im Array zahlen
aufsummiert. Die Eingabe
let ergebnis = summe([30, 4, 67])
101
in der Konstanten ergebnis
.
func schnittArray(array1:Array<Int>, _ array2:Array<Int>) --> Array<Int>
array1
als auch in array2
enthalten sind. Beispielsweise soll die Eingabe
let array1 = [1, 2, 3, 4, 5]
let array2 = [2, 4, 6]
let derSchnitt = schnittArray(array1, array2)
[2, 4]
in derSchnitt
liefern.
var inhalt = NSArray(contentsOfFile:vollerName) as! Array<Int>
Int
-Werten aus einer Datei einliest. Er funktioniert aber nur, wenn die Datei wirklich nur aus Int
-Werten besteht. Der Aufruf
var inhalt = NSArray(contentsOfFile:vollerName) as Array<AnyObject>
Array<Int>
, wobei alle Werte, die keine Zahlen sind, weggeworfen werden sollen!
Array
einer feste Größe haben, mit dem Sie die korrekten FizzBuzz-Ausgaben für Hunderte, Tausende und sogar Millionen von Zeilen erhalten dürfen.Hinweis: Diese Aufgabe ist extrem schwer! Machen Sie sich keine Sorgen, wenn Sie es nicht ohne Hilfe schaffen!
flachMacher
, die aus einem verschachtelten Array von Int
-Zahlen ein normales macht, zum Beispiel:
flachMacher([5, [3, 4], [2, [1]]])
[5, 3, 4, 2, 1]
.Hinweis: Diese Aufgabe ist extrem schwer! Machen Sie sich keine Sorgen, wenn Sie es nicht ohne Hilfe schaffen!
Versuchen Sie, diese Aufgabe mit den Mitteln der funktionalen Programmierung aus dem fortgeschrittenem Abschnitt 10.6.1 zu lösen!
Hinweis: Diese Aufgabe ist extrem schwer! Sie müssen die Funktionsweise von filter
und reduce
recherchieren und verstehen! Machen Sie sich keine Sorgen, wenn Sie es nicht ohne fremde Hilfe schaffen!