Swift-Check

Nachdem ich privat als OS X-Entwickler und beruflich als iOS-Entwickler mit Swift ein paar Erfahrungen gesammelt und mich mit den bei Apple verfügbaren Vorab-Informationen für registrierte Entwickler zu dem Thema weitgehend vertraut gemacht habe, gibt es ein paar Punkte zu Swift, über die ich schreiben möchte:

Historie

Wie Chris Lattner, der Vater von Swift, auf seiner Homepage schreibt, begann er die Arbeit an Swift ganz allein im Juli 2010. Ende 2011 fingen weitere Leute an, zu Swift etwas beizutragen. Und erst im Juli 2013 wurde Swift ein bedeutendes Thema in Apples Abteilung für die Developer Tools.

Apple hatte Objective-C 2.0, das unter anderem Properties für die Deklaration von Instanz-Variablen einführte und Dot-Syntax als Kurzschreibweise für Accessor-Methoden sowie Fast-Enumeration bei For-Schleifen, bereits 2006 bekanntgeben und im Oktober 2007 mit Mac OS X 10.5 ausgeliefert.

Darum ist der Artikel in Mac & i 4/2014 über die APIs in iOS 8 und OS X 10.10 auf Seite 52 leider nicht ganz korrekt, denn dort wird vermutet, daß die Neuerungen in Objective-C 2.0 auf die Arbeit an Swift zurückzuführen sind. Das ist jedoch unmöglich, weil mit Swift erst 4 Jahre später überhaupt begonnen wurde. Tatsächlich ist es andersherum: Christ Lattner schreibt, Swift habe viele Ideen von Objective-C und anderen Sprachen übernommen.

Im Oktober 2011 veröffentlichte Apple iOS 5 und Xcode 4.2, wodurch ARC (Automatic Reference Couting) und Storyboards Einzug hielten. Vorgestellt wurden diese Features offiziell auf der WWDC 2011 im Juli 2011, was bedeutet, daß sie seit mindestens einem Jahr, also seit 2010 in Arbeit waren. Chris Lattner, der auch Vater des LLVM-Compiler ist, war 2010 LLVM-Architekt bei Apple und löste damit den GCC-Compiler ab. LLVM ermöglichte einige Geschwindigkeitssteigerungen und Feature-Neuerungen, die mit GCC nicht möglich waren. LLVM wurde auf der WWDC 2010 erstmals von ihm erwähnt und legte die Grundlage für ARC in 2011, denn ARC setzt den LLVM technisch voraus. Während also ARC und LLVM für Objective-C eingeführt wurden, begann Chris Lattner auch seine Arbeit an Swift. Die Vermutung der Mac & i, ARC wäre aufgrund einer geplanten Ablösung von Objective-C durch Swift in Objective-C eingeführt worden, nur weil Swift dieses Feature ebenfalls aufweist, ist nicht nachvollziehbar für mich. Vielmehr ging es um ein Feature für Objective-C, das dann sinnvollerweise später auch in Swift verwendet wurde.

Von einer Ablösung von Objective-C durch Swift, wie die Mac & i das sieht, kann ebenfalls keine Rede sein, denn Swift ist sowohl von Objective-C Code als auch von der Objective-C Runtime abhängig. Sämtlicher Code in Cocoa, Cocoa Touch, Foundation, UIKit et cetera ist in Objective-C geschrieben und Swift-Apps sind auf all diese Frameworks angewiesen. Ferner benutzt Swift die Objective-C Runtime sogar bei reinem Swift-Code, was ich weiter unten beschreibe.

Mac & i behauptet, es werde keinen automatischen Konverter von Objective-C nach Swift geben. Wie man in Xcode 6 live erleben kann und wie man auch in der WWDC 2014 Session 401 What's New in Xcode 6 lernen konnte, übersetzt Xcode Objective-C dynamisch nach Swift und umgekehrt, allein schon, um jeweils in der anderen Sprache geschriebene Klassen passend für die aktuell benutzte Sprache darstellen zu können in Headern und beim automatischen Code-Vervollständigen.

Laut Apple ist Swift eine neue innovative Sprache für Cocoa und Cocoa Touch: Swift is an innovative new programming language for Cocoa and Cocoa Touch. Cocoa und Cocoa Touch sind jedoch Objective-C. Swift setzt damit Objective-C zwingend voraus und bietet für die Entwickler nur ein alternatives Entwicklungs-Frontend. Tatsächlich bildet Swift diese Objective-C APIs auf Swift-Syntax ab (mapping). Xcode 6 übersetzt Objective-C Header aus den System-Frameworks dynamisch zu Swift. In diversen WWDC-Beiträgen wird Apple auch sehr deutlich:

Swift ist also als persönliches Projekt von Chris Lattner gestartet. Chris ist bei Apple extrem wichtig in den Bereichen Entwicklungs-Tools, Compiler und Debugger. Da ich keinen zwingenden Grund sehe, der Swift für Apple nötig macht, gehe ich davon aus, daß Apple es sich einfach nicht leisten konnte, einem wichtigen Mann wie Chris seinen Wunsch nach Verwendung und Entwicklung dieser Sprache auszuschlagen. Hätten sie ihm das Projekt verwehrt, wäre er möglicherweise gegangen.

Swift benutzt die Objective-C Runtime

Wie in der WWDC 2014 Keynote gesagt wurde, benutzt Swift die Runtime von Objective-C:

Now, Swift is completely native to Cocoa and Cocoa Touch. It's built with the same LLVM compiler as Objective-C using the same Optimizer and Autovectorizer. And it has the same ARC memory management model. And the same runtime which means that your Swift code can fit right alongside your Objective-C and your C code in the same application.

Runtime sollte nicht mit Virtual Machine verwechselt werden. Eine Laufzeitumgebung (Runtime) hat jede Programmiersprache. Bei C ist es die C-Runtime "crt" mit LibC, bei Objective-C ist es die Objective-C Runtime, die ihrerseits LibC benutzt, weil Objective-C eine Erweiterung von C ist. Die C++ Runtime baut ebenfalls auf der C-Runtime auf. Java enthält in seiner Runtime noch eine virtuelle Machine. Die C-Sprachen und Swift haben in ihrer Runtime keine Virtual Machine.

Objective-C ist dynamischer als andere Sprachen und entscheidet, welcher Code tatsächlich aufgerufen wird, möglichst immer erst zur Laufzeit. Das macht die Sprache sehr flexibel und mächtig. Die Objective-C Runtime kann zur Laufzeit sogar die Implementierung von Methoden austauschen. C++ entscheidet hingegen viel mehr schon zur Compile-Zeit.

Jedes Programm kann nur mit einer Runtime arbeiten. Selbst ein einzeiliges Swift-Programm wie println("world"), das keinerlei Code importiert, enhält jedoch ein __OBJC Segment, das dem Objective-C Runtime System dient, wie man der Doku zu otool entnehmen kann: "-o Display the contents of the __OBJC segment used by the Objective-C run-time system." Darum ist die Swift-Runtime entweder identisch mit der Objective-C Runtime oder eine Erweiterung davon. Das Segment sieht man, wenn man sich den Assembler-Code anschaut. Hello-World für C enthält es nicht, aber Hello-World für Objective-C und Swift enthält es. Alternativ kann man sich auch das Target ".o" Object-File mit otool anschauen und sich speziell das Objective-C Segment mit otool -o ausgeben lassen.

Hat man eine Datei hi.swift, die mit

println("Hello World")

ein komplettes einzeiliges Swift-Programm enthält, dann kann man mit swift -frontend hi.swift -o hi.o das Object-File erzeugen und mit swift -frontend -S hi.swift -o hi.s den Assembler-Code, der als Zwischenschritt zum Object-File erzeugt wird, separat ausgeben. Der String "Hello World" wird im Swift-Fall codiert und ist an der Stelle mit den beiden Zeilen, die .short 108 enthalten sichtbar. Der int-Wert 108 entspricht dem l in Hello. Am interessantesten finde ich jedoch, daß der Swift-String "Hello World" offenbar genau wie ein C-String mit 0 terminiert wird: .short 0.

Die Hello-World Source-Code-Versionen für C (hello.c) und für Objective-C (hello.m) haben den gleichen Inhalt:

#include <stdio.h>

int main(void) {
printf("Hello World");
return 0;
}

Für Objective-C wird dann mit clang hello.m -o helloObjC.o und clang -S hello.m -o helloObjC.s das Object-File beziehungsweise die Assembler-Zwischenstufe erzeugt. Für C mit clang hello.c -o hello.o das Object-File und mit clang -S hello.c -o hello.s die Assembler-Ausgabe.

Die Swift-Runtime wird in WWDC 2014 Session 409 "Introduction to LLDB and the Swift REPL" erwähnt im Zusammenhang damit, daß sie dynamic dispatch macht und wie man zu einem Swift-Programm zur Laufzeit neue Features hinzufügt. In dem Beispiel wird eine Extension (vergleichbar einer Objective-C Category) auf String zur Laufzeit des Swift-Programmes registriert und benutzt. Das erinnert doch sehr stark an Dinge wie die Registrierung einer neuen Methode zur Laufzeit eines Objective-C Programms über die class_addMethod Funktion der Objective-C Runtime.

Generics

Swift hat Generics und Objective-C hat das nicht nötig, weil es die Probleme, die mit Generics gelöst werden, nicht hat. So kann man es am kürzesten sagen.

Die Langfassung sieht so aus: Sprachen, die statisch typisieren, legen die Klasse (Typ, Art, Sorte) eines Objektes schon während des Programmierens fest, genauer gesagt zur Compile-Zeit. Sprachen, die dynamisch typisieren, bestimmen die Klasse eines Objektes erst zur Laufzeit. Bei dynamischen Sprachen interessiert man sich generell direkt für die Fähigkeiten, die ein Objekt hat, und nicht so sehr für den Typ und damit nur indirekt für die Fähigkeiten. Man nennt das Duck-Typing: Wenn es watschelt und quakt und schwimmt wie eine Ente, kann ich mit dem Watscheln und Quaken und Schwimmen arbeiten. Mir kann dabei egal sein, ob es eine Ente oder ein anderer Typ Tier ist. Statisch typisierte Sprachen bestehen hingegen auf Enten.

Nun schreibe ich ein Programm "ZeigWasDuKannst" (oder eine Methode oder Funktion oder was auch immer), das die Ente zum Teich watscheln, dort eine Runde schwimmen und dann quaken läßt. Super bis hier. Und nun kommt ein Frosch, der ebenfalls watscheln, quaken und schwimmen kann so wie die Enten das tun. Den möchte ich mit demselben Programm steuern. Das geht mit Objective-C problemlos, denn die Ente und der Frosch verstehen beide die Kommandos für Watscheln, Schwimmen und Quaken.

Mit statisch typisierten Sprachen geht das erstmal nicht, denn hier sind nur Enten zugelassen in dem Programm, aber keine Frösche. Somit haben streng typisierte Sprachen mit ihrer Typsicherheit, auf die sie so stolz sind, ein Problem: Sie müssen die Methode kopieren und Ente durch Frosch ersetzen. Code-Verdoppelung, bei dem nur Kleinigkeiten anders sind, ist schlecht für die Wartbarkeit. Das haben unsere Freunde von der Typsicherheit auch erkannt. Vergleiche "The Problem That Generics Solve" in The Swift Programming Language.

Um das Dilemma Typsicherheit versus Code-Verdoppelung zu lösen, haben die typsicheren Sprachen Generics (generische Programmierung) eingeführt. Der Code, der "ZeigWasDuKannst" implementiert, bekommt dann nicht mehr Ente oder (exklusives oder) Frosch als Eingabe, sondern einen Typ-Parameter. Der stellt sicher, daß im Code der Typ gleich bleibt, also nicht plötzlich von einer Ente auf einen Frosch gewechselt wird zwischen dem Watscheln und Quaken. Damit lösen typsichere Sprachen ein hausgemachtes Problem, das dynamische Sprachen erst gar nicht haben.

Oracle dokumentiert zum Beispiel für Java als Grund für Generics, daß man damit Typen (Klassen, Interfaces) als Parameter verwenden kann. Das kann Objective-C ganz ohne Generics, weil der zuständige Code erst zur Laufzeit bestimmt wird, und wie unter anderem in "Customization with Class Objects" in "The Objective-C Programming Language" beschrieben wird, sind Klassen als Parameter ein gängiges Vorgehen, um wie bei Generics durch die Übergabe eines variablen Typs Code-Verdopplung zu verhindern. Das ist möglich, weil Objective-C Klassen als Objekte behandelt in voller Absicht mit manchmal überraschenden Vorteilen für das Programm-Design, wie Apple in eben diesem Kapitel schreibt. Das war auch schon in der ersten Version der Dokumentation so zu lesen. Oder in der Open Step Doku für Objective-C. So kann man ein NSMatrix-Objekt mit einer beliebigen Klasse (vorzugsweise einer Subklasse von NSCell) für den Zellen-Typ initialisieren. Und das geht nicht nur in Objective-C, sondern auch in Swift, weil Swift kompatibel zu Objective-C-Klassen ist. Da Swift jedoch auch Generics anbietet, weil es statisch typisieren möchte, ist es ein bißchen Frankensteins Monster: Es kann nämlich auch dynamisch typisieren wie man an NSMatrix sieht: Der Klassen-Parameter der bei der Initialisierung übergeben wird, ist kein Generic. Zur Laufzeit stellt das NSMatrix-Objekt dann erst fest, welchen Typ von Zellen es benutzen soll.

Objective-C hat durch die Verwendung von Klassen-Typen auch eine statische Typisierung, um die Absicht des Programmierers klarer zu machen und dem Compiler zu ermöglichen, dem Entwickler etwas mehr behilflich sein zu können mit automatisierten Vorschlägen für mögliche Methoden und dergleichen. Im Gegensatz zu wirklich statisch typisierten Sprachen kann sich die Klasse aber zur Laufzeit ändern. Das ist zum Beispiel nützlich, um Features zu bestehenden Klassen hinzuzufügen, deren Quellcode man nicht zur Verfügung hat. Und man muß nicht für jede Kleinigkeit Subklassen bilden, um eine Methode hinzuzufügen. Somit kann man außerdem auch Dinge mit Hilfe von beispielsweise Categories implementieren, für die andere Sprachen Mehrfach-Vererbung benötigen.

Geschwindigkeit

Swift verwendet wie Apples andere Sprachen den llvm-Compiler, allerdings nicht mit dem (C-Languages) Clang-Compiler-Frontend, sondern mit dem Swift-Compiler-Frontend. Das Swift-Frontend nimmt eine eigene High-Level-Code-Optimierung vor, das Clang-Frontend nicht. Das llvm-Compiler-Backend nimmt eine Low-Level-Code-Optimierung vor. Durch die zusätzliche Optimierung im Frontend für den Swift-Compiler, können Swift-Programme schneller sein. Interessanterweise erzeugt jedoch Hello-World in der Swift-Version mehr Assembler-Code als in der Objective-C-Version.

Typische Optimierungen, die der Swift-Compiler vornimmt, sind zum Beipiel das Inlining von Methoden-Aufrufen oder das Beseitigen von Methoden-Aufrufen, die nichts tun. Inlining kopiert den Code der Methode an jede Stelle, an der die Methode aufgerufen würde.

Siehe dazu auch die WWDC 2014 Session 404 "Advanced Swift".

Das dynamische Message-Dispatching in Objective-C ergibt keinen großen Overhead im Vergleich zu direkten Funktionsaufrufen, denn bevor die Dispatch-Tabellen durchsucht werden, schaut die Runtime in den Methoden-Caches nach, ob dort ein zu der Message passender Eintrag schon vorhanden ist. Vergleiche "The objc_msgSend Function" im Objective-C Runtime Programming Guide.

Der theoretische Geschwindigkeits-Nachteil, den man mit der dynamischen Zuordnung der Methoden-Implementierung zur Laufzeit in Kauf nimmt, wirkt sich durch das Caching, das die Objective-C Messaging-Funktion vornimmt, in der Praxis kaum aus. Das ist ein Punkt, den viele nicht kennen, obwohl er dokumentiert ist. Und darum hat man mit Objective-C eine dynamische Sprache, die auch schnell ist.

Langsamkeit

In der Theorie könnte Swift also durch all die Compiler-Optimierungen schnell sein. In der Praxis ist das jedoch nicht zu beobachten. In To Swift and back again wird beschrieben, daß Swift Probleme mit Automatic Reference Counting (ARC) hat und zusätzliche Retain-Release-Zyklen einfügt, die eigentlich am Ende eine Effizienz-Steigerung ermöglichen sollten, wenn die Optimierung des Compilers benutzt wird. Es stellte sich jedoch heraus, daß der Compiler diese überflüssigen Zyklen bei der Optimierung nicht immer wieder eliminiert.

Die Runtime von Swift liefert keine zuverlässige Performance wie von Objective-C gewohnt. Offenbar schwankt die Laufzeit sogar, wenn man Libraries anders linkt. Christoffer beschreibt in seinem Artikel, daß Swift 100 oder 1000 mal langsamer läuft in vielen Fällen als Objective-C. Wegen lahmer Compile-Zeiten und langsamer Programm-Ausführung haben sie ihr Projekt sogar wieder zurück nach Objective-C migriert.

Die reale Geschwindigkeit von Swift schwankt demnach zwischen C und einer lähmenden Zähigkeit, derer sich sogar Java 1.0.2 furchtbar geschämt hätte.

Einfach ist anders

Swift ist syntaktisch eine viel komplexere Sprache als Objective-C. Es gibt viel mehr Dinge, die man lernen muß, um die Sprache vollständig zu kennen.

Swift ermöglicht es, viele erklärende Teile im Quellcode wegzulassen. Man muß zum Beispiel nicht zwingend einen Typ angeben, wenn man eine Variable deklariert. Der Compiler erkennt den Typ zur Not einfach am zugewiesenen Wert. Immer wenn ein Name für den Compiler eindeutig zugeordnet werden kann, kann man weitere Erklärungen weglassen. Ob man mit einer lokalen Variablen oder eine Instanz-Variablen zu tun hat, erkennt man nicht mehr am Namen, da die self-Referenz bei Eindeutigkeit optional ist. Bei Objective-C ist der Zugriff auf self.instanzVariable jedoch nur eine Kurzform für die Accessor-Methoden, die für das korrekte Speicher-Management sorgen.

Mancher findet zwar beim Einstieg die eckigen Klammern bei Objective-C gewöhnungsbedürftig, aber Objective-C hat immer eindeutig lesbaren Code. In Swift kann man Code schreiben, der sehr kompakt im Quellcode ist, aber extrem komplex in dem, was er tatsächlich tut. Und das kann Swift sehr schwer lesbar machen.

Da jedes Swift-Programm, das mehr als Hello World! ausgeben möchte, auf Objective-C APIs in Cocoa (Touch) zugreift, kommt man um das grundsätzliche Verständnis von Cocoa-Design-Patterns nicht herum. Es ist sogar so, daß sich die Pattern auch in Swift wiederfinden. Swift ist eine zusätzliche Möglichkeit, Cocoa (Touch) Apps, also iOS- und Mac-Apps, zu schreiben.

Hilfsmittel wie die Playgrounds, in denen man mit Swift-Code herumprobieren kann und sofort das Ergebnis sieht nach jeder Zeile, machen einen spielerischen Einstieg sehr attraktiv. Man tippt Swift-Code und kann bei Schleifen Diagramme sehen, die den Verlauf der Werte über die Zeit zeigen oder die benutzten GUI-Objekte in voller Schönheit. Das wird durch REPL, den Run Evaluate Print Loop ermöglicht, der fortlaufend den Code compiliert und ausführt.

Es gibt jedoch keinen zwingenden technischen Grund, aus dem Playgrounds ein exklusives Feature für Swift sein sollten. Vielmehr sieht es mir danach aus, als wäre diese Exklusivität künstlich, um Swift zu promoten, denn Playgrounds für Objective-C sind machbar. Playgrounds für Objective-C wurden offenbar in nur zwölf Stunden implementiert. Es wäre zu wünschen, daß Apple dies in Xcode nachbessert, auch wenn Herr Lattner dann seine Felle davonschwimmen sieht.

It took me around 12h to get from idea to release (Playgrounds for Objective-C)

Mein Eindruck ist, daß Swift sich hauptsächlich an Programmier-Einsteiger richtet und an Umsteiger von anderen Plattformen. An Einsteiger, weil Hilfsmittel wie Playgrounds zum Lernen durch Probieren ermuntern und man natürlich nicht gleich alle komplexen Features zwingend benutzen muß. An Umsteiger, weil einem in den meisten Fällen die Syntax auf den ersten Blick bekannt vorkommt. Daß die Syntax aber auch sehr kompliziert werden kann, merkt man erst später. Ich möchte behaupten, daß Swift die komplizierteste Syntax aller Programmiersprachen hat. Nicht zuletzt, weil sie sich von überall her hat inspirieren lassen.

Für erfahrene Objective-C Programmierer bringt Swift eigentlich keinen großen Vorteil. Im Gegenteil finde ich die Dynamik von Objective-C sehr nützlich und gutmütiges Verhalten wie sicheres Messaging an Nil-Objekte ermöglichen eleganten Code ohne ständige Abfragen auf Existenz (bei Java: null), da die Nachrichten ignoriert werden und man bei Verwendung von Rückgabewerten sichere Default-Werte bekommt, bei BOOL zum Beispiel ein NO und bei int eine 0 und so weiter.

Lesbarkeit

Ich finde Swift schwieriger lesbar als Objective-C. Swift läßt viele explizite Elemente weg, die einem als Mensch beim Lesen helfen, die aber der Compiler nicht zwingend braucht.

Wo in anderen Sprachen das Semikolon einen Befehl eindeutig erkennbar begrenzt, ist man bei Swift am Zeilenende nicht sicher, ob es in der nächsten Zeile mit dem Befehl weiter geht oder ein neuer beginnt. Man erkennt es nur am Zusammenhang.

Der Verzicht auf das Semikolon macht sich auch zum Beispiel bei Strings negativ bemerkbar, wenn ein String über mehrere Zeilen geht. Dazu muß man bei Swift pro Zeile einen String anlegen und mit + verbinden, während bei Objective-C ein Zeilenumbruch kein Problem ist.

Swift verzichtet an vielen Stellen auf Information, die der Compiler sich ableiten kann. Den Menschen als Programmierer belasten solche fehlenden Informationen jedoch, weil er viel mehr Dinge über den Kontext im Kopf halten muß, weil sie nicht mehr im Code stehen. Dazu gehören unter anderem automatisch erkannte Variablen-Typen. Anstatt klar dort stehen zu haben, daß eine Variable ein String ist, muß man überlegen, was wohl der Rückgabetyp der aufgerufenen Funktion sein mag. Das ergibt zwar kurzen und korrekten Code, aber die Lesbarkeit für den Menschen ist stark reduziert. Swift bürdet dem Entwickler zusätzlichen kognitiven Overhead auf: Man muß im Vergleich zu Objective-C viel mehr zusätzliches Wissen über die aktuellen Hintergründe und Zusammenhänge des Swift-Codes im Kopf haben, um die "einfache" Zeile, die man grad liest, korrekt zu verstehen. Das ist "additional memory load" im Gehirn und gilt als schlecht, was Benutzbarkeit (Usability) angeht.

Aus Entwickler-Sicht wirkt Swift auf mich wie ein akademisches Elfenbeinturm-Projekt. In der Theorie klingt es toll, in der Praxis ist es irgendwie unrund. Man merkt schnell, daß der Vater von Swift auch der Vater der LLVM-Compiler-Suite ist. LLVM ist klasse und mächtig und ermöglicht all die Features von Swift. Aber eine Sprache zu entwicklen mit Features, die man einbaut, weil der Compiler so intelligent ist und das möglich macht, ist betriebsblind. Hier wird übersehen, daß nicht jedes geile Feature auch nützlich ist.

David Owens hat einen Artikel geschrieben, der weitgehend deckungsgleich ist mit meiner Einschätzung: Swift bringt leider nicht wirklich was. Ein Objective-C 3.0 wäre eine bessere Idee als Swift.

Oder wie es Jared Sinclair treffend formuliert:

Swift solves problems that I don’t really have, and the problems I care about most Swift doesn’t solve.

Swift löst Probleme, die ich gar nicht habe, und die Probleme, die für mich wichtig sind, löst Swift nicht.

Valid XHTML 1.0!

Besucherzähler


Latest Update: 25. August 2022 at 19:42h (german time)
Link: warcraft.realmacmark.de/dev/osx_dev_swift_check.php