EinfĂĽhrung in Apples neue Programmiersprache Swift, Teil 1
Seite 4: Benutzerdefinierte Typen, Tupel, Klassen
Benutzerdefinierte Typen
Zu den einfachsten benutzerdefinierten Datentypen gehören Aufzählungen (Schlüsselwort enum). Sie gehören der Spezies der Werttypen an. Eine entsprechendes Beispiel ist der Aufzählungstyp Jahreszeiten:
enum Jahreszeiten {
case FrĂĽhling
case Starkbiersaison
case Sommer
case Oktoberfest
case Herbst
case Winter
}
... der sich wie folgt in Variablen nutzen lässt:
var saison = Jahreszeiten.Oktoberfest
Eine typische Anwendung ist das Abfragen von Aufzählungen in switch-Anweisungen. Um weniger Schreibarbeit zu haben, unterstützt Swift die "Punktnotation". Nur als Hinweis: Da im Folgebeispiel aus der switch-Anweisung mit der Variablen saison der Aufzählungstyp hervorgeht, lässt sich also statt saison.Frühling kürzer .Frühling schreiben:
switch saison {
case .FrĂĽhling:
println("Fahrradfahren")
case .Starkbiersaison:
println("Auf zum Nockherberg")
case .Sommer:
println("Biergärten und Badeseen")
case .Herbst:
println("Bergwandern")
case .Oktoberfest:
println("O'zapft is")
default:
println("Warm anziehen")
}
Man hätte in diesem Fall statt default auch case: Winter ... verwenden können, weil das ohnehin die einzig noch nicht abgedeckte Option ist. Im Gegensatz zu C, C++ ist kein break am Ende jeder case-Anweisung notwendig, es gibt also kein automatisches "Durchfallen" zu den nachfolgenden Fällen. Zum Zug kommt immer genau ein case-Fall oder der Fall default. Ohne default-Zeile, analysiert der Compiler in einer Selektion (= switch), ob es für alle möglichen Werte der Selektion einen passenden case-Fall findet. Falls nicht, beschwert er sich mit einer Fehlermeldung. Wer unbedingt an der Syntax mit "durchfallenden" Selektionen festhalten will, für den hält Swift das Schlüsselwort fallthrough bereit, was so ziemlich das Gegenteil eines break bewirkt.
Selektionsanweisungen können noch wesentlich komplexer sein als im Beispiel. Aus Platzgründen sei aber auf die Sprachreferenz verwiesen. Der Vollständigkeit halber: Elemente einer Aufzählung können Argumente besitzen wie im folgenden Beispiel aus der Swift-Dokumentation:
enum Barcode {
case UPCA(Int, Int, Int)
case QRCode(String)
}
Tupel als Behälter
In Swift existieren Tupel, um mehrere, miteinander in Beziehung stehende Daten in ein einfaches Konstrukt zu packen. NĂĽtzlich sind Tupel vor allem fĂĽr Funktionsresultate, weil sich dadurch mehrere Ergebnisse zurĂĽckgeben lassen. Die Erzeugung eines neuen Tupels ist denkbar einfach. Bezeichner wie kto in der rechten Seite sind rein optional:
let konto = (kto: 2314567, blz: 70156666, art: "Giro")
Der Empfänger dieses Tupels könnte dessen Komponenten wie im nachfolgenden Code auslesen. Den Konstanten auf der linken Seite der let-Anweisung wie kontoArt werden dabei die entsprechenden Elemente des Tupels zugewiesen
let (kontoNummer, bankLeitZahl, kontoArt) = konto
Mit Struktur und Klasse
Oberflächlich ähneln sich auf der einen Seite benutzerdefinierte Verbundtypen (struct) und auf der anderen Seite benutzerdefinierte Klassen (class). Sie unterscheiden sich hauptsächlich darin, dass
struct-Vereinbarungen Wertobjekte darstellen, die das Laufzeitsystem bei Übergaben kopieren muss. Die Übergabe von class-Objekten erfolgt dagegen per Referenz. Strukturen können sich nicht von anderen Strukturen ableiten, auch das ein signifikanter Unterschied zwischen Klassen und Strukturen. Betrachtet sei zunächst ein exemplarischer Verbundtyp (struct):
struct Language {
var name: String // name of language
var year: Int // year of publication
func info() -> String {
return "My name is \(name) and I went public in \(year)"
}
}
let swift = Language(name : "swift", year : 2014)
print(swift.info())
In dem Beispiel ist ein Verbund Language definiert, der zwei Eigenschaften besitzt: den Namen einer Programmiersprache und das Jahr ihrer Veröffentlichung. Für die beiden Eigenschaften name und year bedarf es in diesem Fall mangels Initialisierung an der Vereinbarungsstelle einer expliziten Typangabe. Strukturen können Eigenschaften enthalten und ebenfalls Funktionen wie im Beispiel die Funktion info() -> String, die mit Hilfe von Interpolation eine Beschreibung erzeugt und zurückliefert.
Funktionen von Strukturen dürfen standardmäßig keine Eigenschaften ändern. Wer Änderungen dennoch erlauben möchte, muss der entsprechenden Funktionsdeklaration das Schlüsselwort mutating voranstellen, zum Beispiel:
// Irgendwo in einem struct:
// Enter at your own Risk!
mutating func rename(newName : String) -> String
{
self.name = newName
return newName
}
Apropos Zugriff auf Eigenschaften einer Instanz: Zugriffsattribute wie public, protected und private sind in Swift nicht vorhanden, falls man danach suchen sollte.
Zum Kreieren neuer struct- oder class-Objekte ist kein new-Operator notwendig. Dadurch schwindet der idiomatische Unterschied zwischen benutzerdefinierten und fest eingebauten Datentypen noch mehr.
Der Name der zu instanziierenden Struktur (oder Klasse) lässt sich in Swift als Factory-Methode interpretieren. Bei ihrem "Aufruf" übergibt der "Aufrufer" eine Parameterliste, mit der sich die Eigenschaften initialisieren lassen.
Durch die Angabe der Namen der entsprechenden Eigenschaften, wie in Language(name : "swift", spread : 2014) zu sehen, können Entwickler den Code zum einen lesbarer gestalten und zum anderen auch sicherer, da der Übersetzer eindeutig ermittelt, auf welche Eigenschaft der Struktur beziehungsweise auf welches Argument einer Funktion sich der entsprechende Übergabewert bezieht. Alternativ kann man auch eine entsprechende init()-Initialisierungsfunktion schreiben.
Klassengesellschaft
Klassen sind das wichtigste Konstrukt in objektorientierten Programmiersprachen. Als Beispiel soll aus aktuellem Anlass die Klasse WMSpieler dienen.
class WMSpieler {
var land : String = "Deutschland"
var nummer : Int
var name : String
init(nummer : Int, name: String) {
self.name = name
self.nummer = nummer
}
func info() -> String {
return "Der Spieler mit der Nummer \(nummer) heiĂźt \(name)."
}
}
Die Klasse hat drei Eigenschaften, von denen land initial mit "Deutschland" belegt ist. Es gibt auch die Möglichkeit, diese Eigenschaft als Konstante mit let zu vereinbaren, sofern der Wert immer konstant bleibt. Die Initialisierung der beiden anderen Felder, name und nummer, geschieht mit der Initialisierungsfunktion init(). Dieser übergibt der Aufrufer zwei Argumente gleichen Namens, also name und nummer. Um zwischen den aktuellen Argumenten und den gleichnamigen Eigenschaften zu unterscheiden, benutzt die Initialisierungsfunktion das Schlüsselwort self, das eine Referenz des Objekts auf sich selbst enthält. Mit self.name ist daher die Eigenschaft gemeint und mit name der übergebene Parameter.
init kommt also eine besondere Bedeutung zu, weshalb kein Präfix func notwendig ist. Dadurch heben sich Initialisierungsfunktionen von normalen Funktionen ab. Der Zugriff auf die Beispielsklasse könnte wie folgt aussehen:
let neuer = WMSpieler()nummer : 1, name : "Manuel Neuer"
println(neuer.info)
Die explizite Angabe der Parameternamen bei Funktionsaufrufen ist nicht unbedingt erforderlich, kann also wie in C oder C++ "anonym" erfolgen, sofern sich die aktuellen Aufrufparameter mit der definierten Parameterliste in Übereinstimmung befinden. Zur besseren Lesbarkeit empfiehlt es sich aber, die explizite Angabe vorzuziehen, speziell bei Parameterlisten mit Überlängen oder fehlender Übersichtlichkeit.
Es kann nicht nur eine Initialisierungsvariante geben, sondern mehrere Initialisierungsfunktionen mit unterschiedlichen Parameterlisten. Deren Aufruf erfolgt nach dem Kreieren des Objekts und vor seiner Nutzung. init() dient dazu, benötigte Ressourcen wie Datenbankverbindungen oder geöffnete Dateien zu akquirieren.
Selbst wenn Entwickler keine init()-Methode schreiben, ist die Initialisierung eines Objekts sichergestellt. Jede Klasse oder Struktur besitzt automatisch eine standardmäßige, parameterlose Initialisierungsfunktion, die alle angetroffenen Dateneigenschaften mit einem initialen Wert belegt. Eine Ganzzahl etwa mit 0, eine Zeichenkette mit "".
Eine Initialisierungsfunktion darf auch andere Initialisierungsmethoden der gleichen Klasse aufrufen.
class Entwickler {
var Int berufsJahre
var String name
init(String name) {
self.name = name
}
init(string name, berufsJahre) {
self.berufsJahre = berufsJahre
init(name) // Aufruf der ersten init-Funktion
}
}
In jedem Fall sollten Entwickler bei expliziter Ressourcenallokation innerhalb der init-Funktion auch eine Aufräumfunktion definieren, die diese Ressourcen unmittelbar vor der Objektzerstörung wieder in die Freiheit entlässt. Aufräumfunktionen haben die Signatur deinit() und besitzen weder Argumente noch ein Resultat. Initialisierungsfunktionen gibt es sowohl in Klassen als auch in Strukturen, während sich Aufräumfunktionen nur in Klassen heimisch fühlen.
Swift unterstützt auch Struktur- beziehungsweise Klasseneigenschaften und -funktionen, die unabhängig von Instanzen, also global funktionieren (oft Klassenmethoden und Klasseneigenschaften genannt). Die Regel lautet: In Referenztypen wie einer Klasse stellen Entwickler der Funktions- oder Eigenschaftsdefinition das Schlüsselwort class voran, bei Strukturen static:
class WMSpieler {
class var spielerZahl = 0
...
}
Auf diese Eigenschaft lässt sich dann über WMSpieler.spielerZahl zugreifen, ohne dass dazu eine Instanz existieren müsste.