zurück zum Artikel

Einführung in Apples neue Programmiersprache Swift, Teil 2

Dr. Michael Stal

Hier geht es um die komplexeren Sprachmerkmale von Swift, mit denen sich erst das ganze Potenzial von Apples neuer Programmiersprache nutzen lässt.

mikelane45 (CanStockPhoto)

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 [1] 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.

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())

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.

Wer Swift-Beispiele genauer betrachtet, hat unbewusst das Gefühl, dass hier etwas fehlt. Ausnahmen (Exceptions) und Try-catch-Blöcke gibt es nicht. Swift implementiert stattdessen das Null Object Design Pattern [2] und nennt das Ergebnis Optional, und zwar deshalb, weil es bestehende Datentypen um ein Element erweitert. Bei funktionalen Programmiersprachen ist dieses Pattern ein Beispiel für eine Monade. An einer leicht verständlichen Erklärung von Monaden haben sich schon viele versucht, weshalb der Autor es erst gar nicht probiert, sondern den Schwarzen Peter an Wikipedia [3] weitergibt. Wenn Optional die Lösung ist, was genau ist das Problem? Es sind Abfragen wie die folgende:

// Code aus der OO-Steinzeit:
var channel = receiver.connect("Scotty")
if channel != 0
{
var sendung = receiver.send(channel, "Beam me up, Scotty")
if sending == nil
...
}
else
{
println("Faszinierend!+-")
}

Hier gibt es zwei unterschiedliche Fälle der Ergebnisbehandlung. Im Erfolgsfall, also im then-Zweig, kann der Code die zurückgelieferte Information weiter verarbeiten. Der Fehlerfall ist separat zu behandeln. Er ist im else-Zweig angedeutet.

Oft liefern aufgerufene Funktionen individuell festgelegte Fehlerwerte wie 0, -1 oder 42 zurück. Wünschenswert wären hier mehr Vereinheitlichung und Transparenz. Und genau das leistet Optional. Zu jedem Datentyp gibt es eine Optional-Version. Den Einsatz von Optional-Typen erkennen Entwickler daran, dass dem Basistypnamen ein Fragezeichen (oder ein Ausrufezeichen) angehängt wird. Als Beispiel sei Int? genommen, das als Resultatstyp der Methode connect fungieren soll:

var channel = receiver.connect("Scotty")

Ob channel einen gültigen Wert enthält, lässt sich mittels if-Anweisung überprüfen, weil channel einen booleschen Wert zurückliefert, nämlich bei Fehlern false und ansonsten true:

if channel {
// erfolgreicher Verbindungsaufbau
receiver.send(channel!, "Beam me up, Scotty")
}
else {
// Fehler aufgetreten
}

Am besten sind Optionals verständlich, wenn man sie sich als Aufzählungstyp vorstellt. Entweder ist der Wert eines Optional eine Instanz des gewählten Basistyps wie im Beispiel Int oder nil. Letzteres hat keinen Bezug zu NULL in anderen Sprachen, darf also nur im Kontext von Optionals auftauchen. Beispielsweise ist es nicht erlaubt, einer Referenzvariablen nil zuzuweisen. Ganz im Gegensatz zu NULL, das nicht nur für die Initialisierung von Referenzen essenziell ist. Der Default-Wert bei der Vereinbarung von Optionals ist selbstredend nil.

Beim Lesen des obigen Beispielcodes ist das Optional[ sichtbar, weil zum Zugriff auf den Inhalt von channel ein Ausrufezeichen anzuhängen ist:

receiver.send(channel!, "Beam me up, Scotty")

Um sich das zu ersparen, benutzen Entwickler ein implizit enthülltes Optional (Implicitely Unwrapped Optional). Dazu hängt sie bei der Typvereinbarung beim Datentyp, den sie optional machen möchte, statt des Frage- ein Ausrufezeichen an:

var x : String! = .... 
println(x)

Nun entfällt also das Ausrufezeichen bei jedem Zugriff auf x. Ein Problem bleibt aber noch – das der verketteten Aufrufe. Hierzu ein triviales Ausgangsszenario:

class Kunde {
var adresse : Adresse?
}

class Adresse {
var strasse : Strasse?
}

class Strasse {
var schufaEinschätzung : Int
}

Angenommen, dass zum fehlenden Glück noch Pech hinzukommt, weil alle auftretenden Optional-Typen nicht zur Menge der Implicitely Unwrapped Conditionals gehören. Theoretisch wäre bei der Abfrage der Kreditwürdigkeit daher wie folgt vorzugehen:

var kunde : Kunde? = ...
if kunde { // es gibt sie oder ihn ?
var adresse = Kunde.adresse
if adresse { // Heureka
var strasse = andresse.strasse
if strasse { // Sie haben Ihr Ziel erreicht
return strasse.schufaEinschätzung
}
else { // Fehler in Strasse
}
}
else { // Fehler in Adresse
}
}
else { // Fehler in Kunde
}

Das macht keinen Spaß, weil es in jedem Zweig die Abfrage geben muss, ob sie ein gültiges Ergebnis oder einen Fehler zurückliefert. Daher lässt Swift eine Abkürzung zu, die sich Optional Chaining nennt. Würde sich der Aufrufer zum Beispiel für die Straße eines Kunden interessieren, könnte er stattdessen schreiben:

if let strasse = kunde?.adresse?.strasse {
/* auf Google Maps anzeigen */
}

In diesem Fall meldet der Aufruf entweder ein gültiges Ergebnis oder einen Fehler, der auf dem Weg bis zum letzten Zugriff in der Kette aufgetreten ist. Die Zwischenstufen sind nicht mehr zu behandeln. Statt eines Baums von Abfragen gibt es durch dieses Linearisieren nur noch eine einzige Abfrage.

Das if-let-Konstrukt im obigen Beispiel heißt Optional Binding und schlägt zwei Fliegen mit einer Klappe. Zum einen erfolgt im Konstrukt die Überprüfung, ob überhaupt ein gültiges Resultat vorliegt, also ungleich nil. Zum anderen wird eine lokale Konstante in diesem Fall mit dem Resultat des Funktionsaufrufs initialisiert. strasse ist dann im then-Zweig sichtbar und gültig.

Was ist von diesem Ansatz im Zusammenhang mit Fehlerbehandlung zu halten? Im Internet gibt es zurzeit heftige Diskussionen darüber, ob Exception Handling oder Optionals der bessere Ansatz zur Fehlerbehandlung ist. Für benutzerdefinierte Ausnahmen ist der Optional-Mechanismus jedenfalls eine gute Lösung. Der Autor hegt aber seine Zweifel, dass dies auch für Systemausnahmen ein guter Weg ist.

Neben Funktionen stellt Swift Closures zur Verfügung. Genau genommen bilden Closures eine Obermenge von Funktionen. Sie sind Code-Blöcke mit abgeschlossenem Sichtbarkeitsbereich, die sich wie Funktionen beispielsweise als Parameter oder als Variablen nutzen lassen. Sie können dabei auf alles zugreifen, was in der Nachbarschaft ihrer Vereinbarungsstelle zugreifbar ist, und bilden daher einen abgeschlossenen Gültigkeitsraum. Deswegen die Bezeichnung "Closure". Ihre Anwendung ist überall dort sinnvoll, wo konventionelle Funktionen zu aufwendig wären. Ein typisches Einsatzszenario ist die map-Funktion für Container-Datentypen. Sie iteriert über den übergebenen Container, wendet auf jedes gefundene Element eine Funktion beziehungsweise ein übergebenes Closure an und sammelt die Ergebnisse in einem Container, den sie an den Aufrufer zurückliefert:

let intArray = [0,1,2,3,4,5,6,7]

let squares = intArray.map {
(n) -> Int in
n * n
} // squares = [0, 1, 4, 9, 16, 25, 36, 49]

Ein return ist in diesem Fall nicht nötig, da der Compiler den letzten Ausdruck innerhalb des Closures als Ergebnis interpretiert. n bezeichnet eine lokale Variable, in der das Closure auf den von außen übergebenen Parameterwert zugreifen kann. In Beispiel hier übergibt map das aktuell gelesene Element als Parameter n an das Closure. Das Schlüsselwort in legt den Gültigkeitsbereich des Parameters fest. Closures werden in geschweiften Klammern notiert. Da in diesem Fall der Aufrufparameter von map, (d. h. das Closure) leicht abzugrenzen ist, sind noch nicht einmal die runden Klammen des Funktionsaufrufs notwendig.

Es wäre auch möglich gewesen, eine Funktion square() zu vereinbaren und sie als Parameter an map() zu übergeben. Das ist aber reiner "Overkill", wenn die Funktionalität nur an dieser einen Stelle benötigt wird und daher unnötig den Namensraum aufblähen würde.

Es geht sogar noch eleganter und kürzer, wie die Funktion sort() demonstriert, die Felder von Gleitkommazahlen sortiert. Als erstes Element erwartet die Funktion ein Feld von Gleitkommazahlen, als zweiten Parameter den gewünschten Vergleichsoperator, im nachfolgenden Beispiel den Kleiner-als-Operator:

var result =
sort(doubleArray,{(x:Double, y:Double)->Bool in return x < y})

Der Compiler kann hier unschwer feststellen, welche Signatur das Closure besitzen muss, weshalb sich das Beispiel noch kürzer schreiben lässt:

var result = sort(doubleArray, {x , y in x < y})

Leider ist das immer noch zu lang, und lässt sich jedoch durch nummerierte Argumente weiter abkürzen. $0 steht dabei für das erste übergebene Argument, $1 für das zweite, und so weiter:

var result = sort(doubleArray, {$0 < $1})

So ist das schon erheblich kompakter, aber es gibt noch einen letzten Optimierungsschritt:

var result = sort(doubleArray, < )

Das Prinzip lautet also, den Compiler arbeiten zu lassen statt die Entwickler. Es sollte Entwicklern aber immer bewusst sein: Closures und Funktionen können ausschließlich auf Elemente zugreifen, die an ihrem Definitionsort sichtbar sind, nicht aber auf die in der Ausführungsumgebung.

Klassen oder Strukturen sind nicht die einzigen Konstrukte zur Definition oder Erweiterung von Datentypen. Mit dem Schlüsselwort protocol definieren Entwickler Protokolle, die sich von Klassen, Verbundtypen und sogar Aufzählungstypen implementieren lassen. Zudem ist es möglich, Typen mit Extensions zu erweitern – ein Mechanismus, den Sprachdesigner im Allgemeinen als Mixin bezeichnen. Aber nun eines nach dem anderen.

Protokolle lassen sich grob mit Java-Schnittstellen oder Scala-Traits vergleichen. Ein Protokoll repräsentiert eine Rolle. Es schreibt zum Beispiel Methoden und Eigenschaften vor, die Klassen oder Verbundtypen implementieren müssen, wollen sie die Rolle unterstützen. Das Protokoll selbst enthält keine Implementierung, sondern dient lediglich als Blaupause. Klassen oder Strukturen können ein oder mehrere Protokolle unterstützen, indem sie in ihrer Klassendefinition die jeweiligen Protokolle spezifizieren und die entsprechenden Methoden und Eigenschaften dieser Protokolle implementieren. Nach außen präsentiert sich ein Protokoll als ein Referenz-Datentyp und lässt sich auch so nutzen. Hier ein einfaches Beispiel mit dem Protokoll IstOnlineVerfügbar:

protocol IstOnlineVerfügbar {
func url() -> String // einziges Element des Protokolls
}

class HeiseZeitschrift : Zeitschrift, IstOnlineVerfügbar {
/* ... einige Details ausgelassen ... */
var link : String
func url() -> String { // hier ist die Implementierung des Protokolls
return link
}
init(titel : String, ... , url : String) {
/* selbstredend nur getesteter Code ? */
}
}

Hat eine Klasse sowohl eine Elternklasse als auch Protokolle wie die Klasse HeiseZeitschrift, ist die Elternklasse in der Kindklasse als Erstes zu spezifizieren, gefolgt durch die mit Komma getrennten Protokolle. Die Initialisierung der Eigenschaft link im Beispiel erfolgt innerhalb der init-Methode. Anschließend lässt sich das Protokoll wie folgt als Datentyp nutzen:

var iX = HeiseZeitschrift("iX",....)
var online : IstOnlineVerfügbar = iX
println(online.url) // => http://www.heise.de/ix/

Was schenkt man jemand, der schon alles hat? Erweiterungen! Als extension bezeichnet Swift Erweiterungen, die Entwickler einem Datentyp nachträglich hinzufügen können. Andere Programmiersprachen wie Ruby oder Scala offerieren solche Mixins ebenfalls. Extensions sind auch für Grundtypen wie Int und String anwendbar und können das ganze Spektrum von Vereinbarungen inkludieren, zum Beispiel auch Initialisierungsfunktionen, Eigenschaften oder Methoden. Hier ein einfaches Beispiel mit einer (zugegeben überflüssigen) Erweiterung für Ganzzahlen:

extension : Int {
func addMagicNumber() {
self = self + 42
}
}

0.addMagicNumber() // liefert 42

Wie das Beispiel zeigt, "fühlen" sich die Erweiterungen für den Nutzer so an, als wären sie inhärenter Bestandteil des erweiterten Datentyps.

Sowohl der Swift- als auch der Objective-C-Compiler erzeugen als Zwischenschritt LLVM-Bytecode, der sich danach effizient auf verschiedene Hardwarearchitekturen abbilden lässt – derzeit allerdings im Falle von Swift nur auf Systeme mit iOS oder OS X als Betriebssystem. Obwohl es der Name suggeriert (LLVM hieß ursprünglich Low Level Virtual Machine) spezifiziert LLVM keine virtuelle Maschine, sondern einen Compiler, der aus Bytecode nativen Code für die Ausführungsplattform generiert. Bytecode ist Code für eine theoretische Hardware, die sich möglichst nah an verbreitete Hardwarearchitekturen anlehnt. Compilerbauer können sich daher im Wesentlichen auf das Erstellen des Frontends beschränken, während sich LLVM um das Backend kümmert. Wie kommt jetzt Bytecode ins Spiel? Das Frontend ist dafür verantwortlich, aus Swift-Dateien Bytecode zu erzeugen und das Ergebnis an LLVM zu übergeben. Swift liefert also ausführbare Programme und keine virtuelle Maschine, die Bytecode interpretiert.

Objective-C und Swift teilen sich neben LLVM sogar das gleiche Laufzeitsystem, was die Interoperabilität zwischen Kompilaten beider Sprachen deutlich vereinfacht. LLVM ist Open-Source-Software, genauso wie der von Apple benutzte Clang-Compiler (einer Alternative zu GCC), was die ganze Sache auch für Nicht-Apple-Entwickler spannend machen könnte, aber das ist spekulativ.

Die Speicherbereinigung in der Laufzeitumgebung von Swift erfolgt durch automatisches Referenzzählen (ARC = Automated Reference Counting). Zu jedem Objekt im Stapelspeicher gibt es einen Referenzzähler, den die Laufzeitumgebung automatisch initialisiert und aktualisiert. Das macht das Programmieren ein ganzes Stück sicherer, aber ganz ohne Mitdenken funktioniert es dennoch nicht. Wenn zwei Objekte gegenseitig aufeinander verweisen, blockieren sie ihre Vernichtung. Um solche Probleme zu vermeiden, gibt es in Swift zwei Möglichkeiten. Entweder definieren Entwickler die entsprechende Referenz in einer Richtung als weak (schwach). Auf schwache Referenzen muss das Laufzeitsystem nicht achten. Dadurch wird quasi eine Richtung des Zyklus durchbrochen. Oder sie markiert eine Referenz mit dem Schlüsselwort unowned – für diese Referenz wird der Referenzzähler erst gar nicht inkrementiert.

Es gibt noch viele weitere Aspekte von Swift zu entdecken. Etwa Delegates – die Weiterleitung von Verantwortlichkeiten an ein Delegate –, Subscripts – Array-Notation für eigene Collection-Datentypen –, der Schutz von Elternklassenmethoden vor dem Überschreiben in abgeleiteten Klassen, Typkonvertierungen, verschachtelte Typdefinitionen und noch einiges andere mehr.

Wer also Swift näher kennen lernen möchte, sollte wissen, dass Mitglieder des (kostenpflichtigen) Apple-Developer-Netzwerks schon jetzt auf eine frühe Version von Swift als Bestandteil von Xcode 6 Zugriff haben, ebenso auf Beta-Versionen von iOS 8 und OS X Yosemite. Die Mitgliedschaft kostet jährlich 99 US-Dollar. Es ist davon auszugehen, dass die produktreife Implementierung der Sprache zeitgleich mit den neuen Betriebssystemversionen im Herbst erscheinen dürfte.

Für Objective-C-Programmierer aus dem OS-X- und iOS-Umfeld ergibt sich durch Swift die Chance, auf eine neue und moderne Sprache zu wechseln, die nicht den Ballast mehrerer Jahrzehnte auf ihren Schultern trägt, die aber sicherlich noch einige Ergänzungen und Änderungen erfährt. Die vollmundigen Präsentationen, die Swift zur besten Entwicklungsplattform küren, sollte man nicht ganz so ernst nehmen. Jedoch enthält die Sprache einige innovative und neue Konzepte. Aber genau genommen kombiniert sie geschickt die existierenden Konzepte aus konkurrierenden Plattformen wie Java oder .NET, freilich ohne deren Ballast mitschleppen zu müssen. Ein früherer Bundeskanzler hätte dies als "Gnade der späten Geburt" bezeichnet.

Interessant dürfte Swift für solche Entwickler sein, die derzeit primär auf Grundlage von Java oder .NET Apps für konkurrierende Plattformen entwickeln. Durch die Nähe zu den genannten Sprachen begradigt Swift die Lernkurve gehörig und erhöht dadurch die Attraktivität des Apple-Ökosystems für "migrationswillige" Entwickler.

Spannend ist der Ansatz, Open-Source-Software wie LLVM und Clang als Frontend für die Generierung nativen Codes zu nutzen. Vor allem, wenn man eine Öffnung von Swift für andere Betriebssysteme ins Auge fassen sollte. Damit kein YaL-Effekt (Yet another Language) entsteht, sollte Apple die Entwicklergemeinde ins eigene Boot holen. Bisher haben nur solche Sprachen den Durchbruch zum Mainstream geschafft, die von ihren Schöpfern exzellent unterstützt und motiviert wurden. Das Über-den-Zaun-werfen funktioniert bei neuen Programmiersprachen noch viel weniger als sonst ohnehin schon. Apple sollte also das neue Apfelbäumchen hegen und pflegen. Nur dann könnte es schon bald heißen: "Leb' wohl, Objective-C."

Michael Stal
ist bei der Corporate Technology der Siemens AG als Program Manager und Certified Senior Software Architect tätig. Er beschäftigt sich mit verteilten Systemen und Softwarearchitekturen und ist unter anderem Koautor der Buchserie Pattern-Oriented Software Architecture.
(ane [4])


URL dieses Artikels:
https://www.heise.de/-2264257

Links in diesem Artikel:
[1] https://www.heise.de/hintergrund/Einfuehrung-in-Apples-neue-Programmiersprache-Swift-Teil-1-2260287.html
[2] http://en.wikipedia.org/wiki/Null_Object_pattern
[3] http://en.wikipedia.org/wiki/Monad_%28functional_programming%29
[4] mailto:ane@heise.de