Einführung in Apples neue Programmiersprache Swift, Teil 2

Sprachen  –  1 Kommentare
mikelane45 (CanStockPhoto)

Hier geht es um die komplexeren Sprachmerkmale von Swift, mit denen sich erst das ganze Potenzial von Apples neuer Programmiersprache nutzen lässt: das Ableiten neuer Klassen, Protokolle als Schnittstellenkonzept, Erweiterungen durch Mixins, parametrisierte Datentypen, Optionals, Closures und Speicherverwaltung.

Der erste Teil gab zunächst einen groben Überblick über die Hintergründe von Swift, um dann tiefer in die grundlegenden Elemente der Sprache abzutauchen. Der Beitrag endete mit dem Klassenkonzept von Swift.

Von Eltern und Kindern

Swift-Klassen können maximal eine Elternklasse besitzen, von der sie Methoden und Eigenschaften erben. Für folgende Klasse Zeitschrift

class Zeitschrift {
var verlag : String
var titel : String
init(titel : String, verlag : String) {
self.verlag = verlag
self.titel = titel
}
func details() -> String {
return "Die Zeitschrift \(titel) wird herausgegeben ↵
von \(verlag)."
}
}

lässt sich eine davon abgeleitete Klasse definieren:

class HeiseZeitschrift : Zeitschrift {
var ausgabenProJahr : Int
init(titel : String, verlag : String, ausgaben : Int){
super.init(titel, "Heise")
ausgabenProJahr = ausgaben

}
override func details() -> String {
return super.details() + " Zahl der Ausgaben pro Jahr: ↵
\(ausgabenProJahr)"
}
}

Im obigen Beispiel leitet sich die Kindklasse HeiseZeitschrift von der Elternklasse Zeitschrift ab. Die Elternklassen-Methode details() übernimmt die Kindklasse in diesem Fall nicht, sondern überschreibt sie mit ihrer eigenen Version, was am Schlüsselwort override sichtbar ist. Diese explizite Festlegung durch override soll sicherstellen, dass Entwickler nicht in Kindklassen eine gleichnamige Methode erstellen, die dann unbeabsichtigt die entsprechende Methode der Elternklasse verdeckt.

Von ihrer Elternklasse erbt HeiseZeitschrift die Eigenschaften verlag und titel. Zunächst sorgt die
Initialisierungsfunktion der abgeleiteten Klasse über das Schlüsselwort super für die Anfangsbelegung der Elternklassen-Anteile, bevor sie ihren eigenen Zustand definiert – über super sind generell alle Elemente der Basisklasse erreichbar. Diese Reihenfolge ist empfehlenswert, damit die Kindklasse auf gültige Daten der Elternklasse zugreifen kann, etwa dann, wenn sich Eigenschaften der Kindklasse aus denen der Elternklasse berechnen beziehungsweise von diesen abhängen. In manchen Beispielen halten sich die Autoren nicht an diese Reihenfolge, was Entwickler nicht nachahmen sollten, weil es fehlerträchtig ist – Fehler in der Initialisierungsreihenfolge sind erfahrungsgemäß nur schwer aufzuspüren.

Das Kreieren und Nutzen einer neuen Instanz von HeiseZeitschrift gestaltet sich wie folgt

let lieblingsZeitschrift = HeiseZeitschrift("iX", "Heise", 12)
println(lieblingsZeitschrift.details())

Parametrisierte Funktionen und Datentypen

Das "Hello World"-Pendant bei parametrisierten Typen hat die Standardoperationen push()und pop() – hello, Stack! Wer einen Stack von Ganzzahlen benötigt, definiert dazu normalerweise in weniger zeitgemäßen Sprachen eine Klasse IntStack. Zu ihr gesellen sich dann im Laufe der Zeit typischerweise weitere, etwa DoubleStack, CharacterStack und <MyType>Stack. Das erfordert nicht nur langweilige Schreibarbeit, sondern mehrfaches Testen und im Fehlerfall auch mehrfaches Aktualisieren. Umtriebige Entwickler versuchen natürlich, einen Ausweg aus dem Dilemma zu finden, und implementieren am Ende einen abstrakten Datentyp ObjectStack, der beliebige Objekte aufnehmen kann. Andere können diese abstrakte Klasse als Basis für ihre eigenen Stack-Typen verwenden. Passt doch alles, könnte man meinen.

Der Typ ObjectStack erfordert eine Typkonvertierung in abgeleiteten Klassen. Das ist aber nicht typsicher, denn durch die Hintertür könnten Aufrufer beliebige Objekte im Stack platzieren, egal um was für einen Stack es sich handelt. Hier wäre also nur strikte Disziplin der Entwickler ein adäquates Heilmittel; oder kurz gesagt, dieser Ansatz trägt nicht. Abgesehen davon gibt es in Swift keine gemeinsame Wurzelklasse wie in Java oder C#, was diesen Ansatz von vornherein unmöglich macht.

Was tun? Das, was auch C#, Java, Scala, C++ tun, nämlich parametrisierte Datentypen zum Typsystem hinzufügen. Für den Stack hat die Swift-Dokumentation, wenig überraschend, die entsprechende Lösung parat:

struct Stack<T> {
var items = T[]()
mutating func push(item: T) {
items.append(item)
}
mutating func pop() -> T {
return items.removeLast()
}
}

Dort sieht man die Verwendung von mutating, die der entsprechenden Funktion erlaubt, Daten zu modifizieren. Das T ist der Typparameter – es kann auch mehrere solcher Parameter geben. Ein Stack von Ganzzahlen lässt sich jetzt ganz einfach kredenzen:

var meinStack = Stack<Int>()
meinStack.push(42)

Solch eine Schreibweise ist typsicher und effizient. Interessant in diesem Zusammenhang sind sogenannte Beschränkungen, denen der parametrisierte Typ unterliegen kann. Die folgende Typdefinition zeigt das exemplarisch – diesmal anhand einer parametrisierten Funktion. Neben Klassen und Strukturen lassen sich auch Funktionen parametrisieren – eigentlich logisch, da Funktionen auch Datentypen sind.

func lessThan<T: Ordered>(x: T, y : T) {
return x < y
}

In diesem Beispiel steht nach dem abstrakten Typparameter T eine Einschränkung T : Ordered. Ist Ordered eine Klasse, dann besagt die Einschränkung, dass der Typparameter eine Unterklasse von Ordered sein muss. Es könnte sich aber auch um ein Protokoll handeln, das der Typparameter anbieten muss. Protokolle ähneln Java-Schnittstellen, wie eine detailliertere Beschreibung zeigen wird.