Moderne Programmierung mit Swift, Teil 2

Seite 2: Initializer-Arten und Vererbung

Inhaltsverzeichnis

Bei Reference Types werden alle Initializer immer einer von zwei Arten zugeordnet: Designated oder Convenience. Ein Designated Initializer bewirkt, dass nach seinem Aufruf alle notwendigen Eigenschaften für eine neue Instanz des zugrundeliegenden Typs gesetzt sind. Er muss imstande sein, ohne Aufruf eines anderen Initializers das Objekt vollständig zu erstellen. Ein Aufruf anderer Initializer, wie im vorherigen Quellcodeausschnitt zu sehen, ist bei Designated Initializern in Reference Types nicht möglich.

Im Gegensatz dazu übernehmen die Convenience Initializer nur einen Teil der Initialisierung und rufen anschließend einen anderen Initializer auf, der weitere Schritte ausführt. Sie können sowohl andere Convenience als auch Designated Initializer desselben Typs aufrufen. Wichtig ist nur, dass am Ende jener Kette in jedem Fall ein Designated Initializer steht. Das gewährleistet, dass die Initialisierung eines Reference Type immer mit dem Aufruf eines Designated Initializer endet, der sicherstellt, dass die neue Instanz vollständig einsatzbereit ist.

Designated Initializer werden in Reference Types genauso erstellt, wie bereits in den vorangegangenen Beispielen der Structures zu sehen war. Convenience Initializer sind mit einem init vorangestellten convenience zu deklarieren.

Würde man die Structure Person aus dem vorangegangenen Listing als Klasse und damit als Reference Type umsetzen, wären die ersten beiden Initializer init() und init(firstName:lastName:) zwingend als Convenience Initializer zu implementieren, da sie beide einen anderen Initializer desselben Typs aufrufen. Der dritte Initializer init(firstName:lastName:age:) stellt den Designated Initializer dar, über den alle nicht-optionalen Properties einen Wert zugewiesen bekommen, wodurch die neue Instanz vollständig initialisiert ist. Das folgende Codebeispiel zeigt die Umsetzung der Structure Person als Klasse in Swift.

class Person {
let firstName: String
let lastName: String

var age: UInt?

convenience init() {
self.init(firstName: "Max", lastName: "Mustermann")
}

convenience init(firstName: String, lastName: String) {
self.init(firstName: firstName, lastName: lastName, age: nil)
}

init(firstName: String, lastName: String, age: UInt?) {
self.firstName = firstName
self.lastName = lastName
self.age = age
}
}

Das Prinzip der Designated und Convenience Initializer spielt bei der Vererbung von Reference Types und der sogenannten Zwei-Phasen-Initialisierung eine weitere wichtige Rolle; mehr dazu später.

Neben den bisher gezeigten Initializern gibt es in Swift noch eine weitere Variante: die sogenannten Failable Initializer. Wie der Name andeutet, handelt es sich dabei um Initializer, die möglicherweise fehlschlagen und in dem Fall keine Instanz des zugrundeliegenden Typs, sondern nil zurückgeben.

Failable Initializer werden statt mit init mittels init? (man beachte das Fragezeichen) deklariert. Sie sind gleichzeitig die einzigen Initializer, die sich via return verlassen lassen und dabei nil zurückliefern können, um so zu signalisieren, dass eine Initialisierung fehlgeschlagen ist.

Ein einfaches Beispiel für einen Failable Initializer zeigt der folgende Codeauszug. Dort wird eine Structure Person mit einer Property name deklariert. Ein zusätzlicher Initializer nimmt einen Parameter für jene Property entgegen. Handelt es sich bei dem übergebenen String aber um eine leere Zeichenkette, wird das als Fehler interpretiert und der Initializer mittels return nil verlassen. Andernfalls wird der Property wie gewohnt der übergebene Wert zugewiesen, wodurch die Initialisierung erfolgreich abgeschlossen ist.

struct Person {

var name: String
init?(name: String) {

if name == "" {
return nil
}

self.name = name
}
}

Reference Types (sprich Klassen) sind in Swift die einzigen Typen, die ihre Eigenschaften und Funktionen vererben können. Wird bei ihrer Deklaration nach dem Name ein Doppelpunkt angeführt, lässt sich im Anschluss daran die Superklasse angeben, von der der neue Typ alle Eigenschaften und Funktionen erben soll. Ein Beispiel dazu zeigt der folgende Codeauszug, in dem drei Klassen deklariert werden: Vehicle, Car und Bicycle. Sie dienen dazu, unterschiedliche Fortbewegungsmittel abzubilden, wobei Vehicle alle Eigenschaften und Funktionen zusammenfasst, die für alle Fahrzeuge gleichermaßen gelten. Car und Bicycle erben von ihr und ergänzen lediglich einige spezifische Eigenschaften für Autos beziehungsweise Fahrräder.

class Vehicle {
var manufacturer: String?
var color: String?
var currentSpeed = 0

func startDriving() {
print("Start driving...")
currentSpeed = 10
}

func stopDriving() {
print("Stop driving...")
currentSpeed = 0
}
}

class Car: Vehicle {
var numberOfDoors: Int?
func hoot() {
print("Hupen!")
}
}

class Bicycle: Vehicle {

var type: String?
func ring() {
print("Klingeln!")
}
}

Subklassen können nicht nur spezialisierte Eigenschaften und Funktionen enthalten, sondern auch die geerbten Eigenschaften und Funktionen überschreiben und durch eigene Logik ersetzen. Dazu implementiert die Subklasse die gewünschte Funktion so, wie sie in der Superklasse angelegt ist, und setzt das Schlüsselwort override voran. Instanzen der Subklasse rufen anschließend die neue Implementierung auf.

Das Überschreiben ist beispielhaft im nächsten Codeausschnitt dargestellt. Die Superklasse Vehicle definiert dort eine Methode printInfo(), die die beiden Subklassen Car und Bicycle überschreiben. Welche der insgesamt drei verfügbaren Implementierungen nun aufgerufen wird, ist davon abhängig, welchem Typ die von der Methode genutzten Instanz entspricht.

class Vehicle {
func printInfo() {
print("Das ist ein Fahrzeug.")
}
}

class Car: Vehicle {
override func printInfo() {
print("Das ist ein Auto.")
}
}

class Bicycle: Vehicle {
override func printInfo() {
print("Das ist ein Fahrrad.")
}
}

Vehicle().printInfo()
// Das ist ein Fahrzeug.

Car().printInfo()
// Das ist ein Auto.

Bicycle().printInfo()
// Das ist ein Fahrrad.

Das Überschreiben von Eigenschaften und Funktionen einer Klasse durch mögliche Subklassen lässt sich auch konsequent verhindern, indem Entwickler sie mit dem Schlüsselwort final deklarieren. Sogar Klassen selbst können mit dem Schlüsselwort versehen werden, was zur Folge hat, dass sich keine Subklassen erstellen lassen.